Socket与系统调用深度分析
Posted richardtao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket与系统调用深度分析相关的知识,希望对你有一定的参考价值。
系统调用
系统调用的过程
系统调用的过程如下:
- 用户程序
- C库(API):INT 0x80
- system_call
- 系统调用服务例程
- 内核程序
说明:
- 我们常说的用户 API 其实就是系统提供的 C 库;
- 系统调用是通过软中断指令 INT 0x80 实现的,而这条 INT 0x80 指令就被封装在 C 库的函数中。
- INT 0x80 这条指令的执行会让系统跳转到一个预设的内核空间地址,它指向系统调用处理程序,即 system_call 函数
下图为系统调用具体的流程。
值得一提的是:系统调用处理程序 system_call 并不是系统调用服务例程。
系统调用服务例程是对一个具体的系统调用的内核实现函数,而系统调用处理程序是在执行系统调用服务例程之前的一个引导过程,是针对 INT 0x80 这条指令,面向所有的系统调用的。
简单来讲,执行任何系统调用,都是先通过调用 C 库中的函数,这个函数里面就会有软中断 INT 0x80 语句,然后转到执行系统调用处理程序 system_call,system_call 再根据具体的系统调用号转到执行具体的系统调用服务例程。
那么,system_call 函数是怎么找到具体的系统调用服务例程的呢?
答案是:通过系统调用号查找系统调用表 sys_call_table。
软中断指令 INT 0x80 执行时,系统调用号会被放入 eax 寄存器中,system_call 函数可以读取 eax 寄存器获取,然后将其乘以 4,生成偏移地址,然后以 sys_call_table 为基址,基址加上偏移地址,就可以得到具体的系统调用服务例程的地址了!
然后就可以根据这个地址找到对应的系统调用服务例程了。
系统调用可以被进程抢占、进入阻塞状态
其原因在于:
系统调用通过软中断 INT 0x80 陷入内核,跳转到系统调用处理程序 system_call 函数,然后执行相应的服务例程。但是由于是代表用户进程,所以这个执行过程并不属于中断上下文,而是进程上下文。
因此,系统调用执行过程中,可以访问用户进程的许多信息,可以被其他进程抢占,可以休眠。
当系统调用完成后,把控制权交回到发起调用的用户进程前,内核会有一次调度。如果发现有优先级更高的进程或当前进程的时间片用完,那么会选择优先级更高的进程或重新选择进程执行。
示例分析
了解系统调用的过程之后,我们举个例子来分析用户态是如何陷入到内核态的。
由于本次实验的目的是调研socket
相关的系统调用,因此我们以一个bind
绑定事件作为例子。
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MY_SOCK_PATH "/somepath"
#define LISTEN_BACKLOG 50
#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main(int argc, char *argv[])
{
int sfd, cfd;
struct sockaddr_un my_addr, peer_addr;
socklen_t peer_addr_size;
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
handle_error("socket");
memset(&my_addr, 0, sizeof(struct sockaddr_un));
/* Clear structure */
my_addr.sun_family = AF_UNIX;
strncpy(my_addr.sun_path, MY_SOCK_PATH,
sizeof(my_addr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *) &my_addr,
sizeof(struct sockaddr_un)) == -1)
handle_error("bind");
if (listen(sfd, LISTEN_BACKLOG) == -1)
handle_error("listen");
在用户态我们调用了
bind
函数,它声明在<sys/socket.h>
。
/* Give the socket FD the local address ADDR (which is LEN bytes long). */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
__THROW;
我们之前提到过,用户态进程只需要调用库函数即可,而不用管这个函数是如何实现的。
linux
中这些具体函数的实现则由glibc
统一提供。它定义在glibc-2.23/sysdeps/unix/sysv/linux/bind.c
int __bind (int fd, __CONST_SOCKADDR_ARG addr, socklen_t len)
{
#ifdef __ASSUME_BIND_SYSCALL
return INLINE_SYSCALL (bind, 3, fd, addr.__sockaddr__, len);
#else
return SOCKETCALL (bind, fd, addr.__sockaddr__, len, 0, 0, 0);
#endif
}
weak_alias (__bind, bind)
在syscall
之前需要先将参数传入寄存器。
之前,如我们之前分析的一样:使用0x80中断去陷入内核,并将返回值存放到eax寄存器中,通常0表示成功。
syscall的name为
__NR_##name
,在本例中即为__NR_bind
。
其定义在/usr/include/asm/unistd_64.h
中。
#define __NR_bind 49
#define __NR_listen 50
#define __NR_getsockname 51
这样,用户态和内核态通过系统调用号(49)来确定本次系统调用是哪个功能。
Socket系统调用分析
Socket系统调用主要完成socket的创建,必要字段的初始化,关联传输控制块,绑定文件等任务,完成返回socket绑定的文件描述符。
socket的调用关系如下:
/**
* sys_socket
* |-->sock_create
* | |-->__sock_create
* | |-->inet_create
* |-->sock_map_fd
*/
sock_map_fd
该函数的主要功能在于:负责分配文件,并实现与socket的绑定。
/* 套接口与文件描述符绑定 */
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
/* 获取未使用的文件描述符 */
int fd = get_unused_fd_flags(flags);
if (unlikely(fd < 0))
return fd;
/* 分配socket文件 */
newfile = sock_alloc_file(sock, flags, NULL);
if (likely(!IS_ERR(newfile))) {
/* fd和文件进行绑定 */
fd_install(fd, newfile);
return fd;
}
/* 释放fd */
put_unused_fd(fd);
return PTR_ERR(newfile);
}
sock_create
其主要功能为:负责创建socket
,并进行必要的初始化工作。
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
之后,sock_create
调用__socket_create
函数来进行必要的检查项,并创建和初始化socket
。
在__socket_create
函数中,调用对应协议族的pf->create
函数来创建传输控制块,并且与socket进行关联。
/* 创建socket */
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
/*
* Check protocol is in range
*/
/* 检查协议族 */
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
/* 检查类型 */
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;
/* Compatibility.
This uglymoron is moved from INET layer to here to avoid
deadlock in module load.
*/
/* ipv4协议族的packet已经废除,检测到,则替换成packet协议族 */
if (family == PF_INET && type == SOCK_PACKET) {
pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)
",
current->comm);
family = PF_PACKET;
}
/* 安全模块检查套接口 */
err = security_socket_create(family, type, protocol, kern);
if (err)
return err;
/*
* Allocate the socket and allow the family to set things up. if
* the protocol is 0, the family is instructed to select an appropriate
* default.
*/
/* 分配socket,内部和inode已经绑定 */
sock = sock_alloc();
if (!sock) {
net_warn_ratelimited("socket: no more sockets
");
return -ENFILE; /* Not exactly a match, but its the
closest posix thing */
}
/* 设定类型 */
sock->type = type;
#ifdef CONFIG_MODULES
/* Attempt to load a protocol module if the find failed.
*
* 12/09/1996 Marcin: But! this makes REALLY only sense, if the user
* requested real, full-featured networking support upon configuration.
* Otherwise module support will break!
*/
if (rcu_access_pointer(net_families[family]) == NULL)
request_module("net-pf-%d", family);
#endif
rcu_read_lock();
/* 找到协议族 */
pf = rcu_dereference(net_families[family]);
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;
/*
* We will call the ->create function, that possibly is in a loadable
* module, so we have to bump that loadable module refcnt first.
*/
/* 增加模块的引用计数 */
if (!try_module_get(pf->owner))
goto out_release;
/* Now protected by module ref count */
rcu_read_unlock();
/* 调用协议族的创建函数 */
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
/*
* Now to bump the refcnt of the [loadable] module that owns this
* socket at sock_release time we decrement its refcnt.
*/
if (!try_module_get(sock->ops->owner))
goto out_module_busy;
/*
* Now that we're done with the ->create function, the [loadable]
* module can have its refcnt decremented
*/
module_put(pf->owner);
err = security_socket_post_create(sock, family, type, protocol, kern);
if (err)
goto out_sock_release;
*res = sock;
return 0;
out_module_busy:
err = -EAFNOSUPPORT;
out_module_put:
sock->ops = NULL;
module_put(pf->owner);
out_sock_release:
sock_release(sock);
return err;
out_release:
rcu_read_unlock();
goto out_sock_release;
}
EXPORT_SYMBOL(__sock_create);
对于PF_INET
协议族来讲,上述的pf->create
函数将调用inet_create
函数。
熟悉设备驱动的同学应该都知道这些操作被定义在file_operations
结构中。
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
最后,在inet_create
函数中完成创建传输控制块,并且将socket与传输控制块进行关联。至于该函数代码这里不再展开分析。
实验代码分析
在上一次实验中,我们已经将老师提供的客户端以及服务器端的程序集成到了MenuOS
中,可以在qemu
虚拟机中使用replyhi
和hello
两个命令。
但是对于这两个程序的内部实现还没有深入分析,这里我们通过阅读代码来找出这两个程序在执行时所使用到的Socket API
。
服务器端
首先,分析一下服务器端的代码。
在我们使用
replyhi
命令时,会执行main.c
文件中的StartReplyhi函数。
MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi);
继续阅读
StartReplyhi
函数的代码。可以发现,在满足连接条件的判断语句中,程序继续调用了Replyhi函数。
int StartReplyhi(int argc, char *argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr, "Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
Replyhi();
printf("Reply hi TCP Service Started!
");
}
else
{
/* parent process */
printf("Please input hello...
");
}
}
查看
Replyhi
函数的实现。在函数中总共调用了六个方法,从字面意思来看,不难推测出它们的具体含义分别是:初始化服务、启动服务、接收消息、发送消息、停止服务以及关闭服务。
这些方法都在头文件
syswrapper.h
中定义。下面将对这些方法逐一展开分析。
int Replyhi()
{
char szBuf[MAX_BUF_LEN] = "