在linux中,将程序的运行空间分为内核空间与用户空间(内核态和用户态),在逻辑上它们之间是相互隔离的,因此用户程序不能访问内核数据,也无法使用内核函数。当用户进程必须访问内核或使用某个内核函数时,就得使用系统调用(System Call)。在Linux中,系统调用是用户空间访问内核空间的唯一途径.
《一》 Socket API编程接口:
(1)API:应用编程接口,即应用程序与系统之间的接口,本质是一些预先定义的函数集合,
功能:应用程序或开发人员可利用API访问系统中的资源和取得 OS 的服务(例如利用API访问一组例程),实现计算机软件之间的相互通信。
(2)Socket:对Socket理解为一种特殊的文件,是对“open—write/read—close”模式的一种实现,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),包括socket()、bind()、listen()、connect()、accept()、read()、write()以及close()等函数.
《二》 系统调用机制:
系统调用:就是一种特殊的接口。通过这个接口,用户可以访问内核空间。系统调用规定了用户进程进入内核的具体位置。
系统调用是用户进程进入内核的接口层,它本身并非内核函数,但它是由内核函数实现,进入内核后,不同的系统调用会找到各自对应的内核函数,这些内核函数被称为系统调用的“服务例程”。比如系统调用getpid实际调用了服务例程为sys_getpid(),或者说系统调用getpid是服务例程sys_getpid()的“封装例程”。
具体步骤:用户进程-->系统调用-->内核-->返回用户空间。
系统调用就是为了解决上述问题而引入的,是提供给用户的“特殊接口”。
系统调用规定用户进程进入内核空间的具体位置。
(1).程序运行空间从用户空间进入内核空间。
(2)处理完后再返回用户空间。
《三》 API与系统调用的区别和联系
(1)区别:API是函数的定义,规定了这个函数的功能,跟内核无直接关系。而系统调用是通过中断向内核发请求,实现内核提供的某些服务。
(2)联系:程序员调用的是API(API函数),然后通过与系统调用共同完成函数的功能。 因此,API是一个提供给应用程序的接口,一组函数,是与程序员进行直接交互的。系统调用则不与程序员进行交互的,它是根据API函数,通过一个软中断机制向内核提交请求,以获取内核服务的接口。
《四》 Socket系统调用过程分析
1、 函数原型:int socket(int domain, int type, int protocol);
2、 内核实现源码分析
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { int retval; struct socket *sock; int flags;
... retval = sock_create(family, type, protocol, &sock); if (retval < 0) goto out; retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); if (retval < 0) goto out_release; out: /* It may be already another descriptor 8) Not kernel problem. */ return retval; out_release: sock_release(sock); return retval; }
下面主要分析sock_create和sock_map_fd这两个函数,看它们是怎么在内核一步步创建和管理我们使用的socket。
(1)sock_create函数
sock_create() 实际调用的是 __sock_create()。
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; ... err = security_socket_create(family, type, protocol, kern);//SElinux相关,暂不关注 /* * 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. */ sock = sock_alloc();//创建struct socket结构体 sock->type = type;//设置套接字的类型 ... pf = rcu_dereference(net_families[family]);//获取对应协议族的协议实例对象 ... err = pf->create(net, sock, protocol, kern); if (err < 0) goto out_module_put; ... err = security_socket_post_create(sock, family, type, protocol, kern);//SElinux相关,暂不关注 *res = sock; ... }
其中sock_alloc()和pf->create()这两个函数比较重要,Sock_alloc()函数分配一个struct socket_alloc结构体,将sockfs相关属性填充在socket_alloc结构体的vfs_inode变量中,以限定后续对这个sock文件允许的操作。同时sock_alloc()最终返回socket_alloc结构体的socket变量,用于后续操作。
pf->create调用的就是inet_create()函数
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern) { struct sock *sk; struct inet_protosw *answer; struct inet_sock *inet; struct proto *answer_prot; unsigned char answer_flags; char answer_no_check; int try_loading_module = 0; int err; ... sock->state = SS_UNCONNECTED; /* Look for the requested type/protocol pair. */ lookup_protocol: err = -ESOCKTNOSUPPORT; rcu_read_lock(); //根据socket传入的protocal在inetsw[]数组中查找对应的元素 list_for_each_entry_rcu(answer, &inetsw[sock->type], list) { err = 0; /* 如果我们在socket的protocal传入的是6,即TCP协议,那么走这个分支 */ if (protocol == answer->protocol) { if (protocol != IPPROTO_IP) break; } else { /* 如果socket的protocal传入的是0,那么走这个分支 */ if (IPPROTO_IP == protocol) { protocol = answer->protocol;//重新给protocal赋值,因此socket中protocal传入的是0或者6,都是可以的 break; } if (IPPROTO_IP == answer->protocol) break; } err = -EPROTONOSUPPORT; } ... sock->ops = answer->ops;//将查找到的对应协议族的协议函数操作集赋值给我们之前创建的socket answer_prot = answer->prot; ... //创建sock结构体,注意这里创建的结构体类型是sock,之前我们通过sock_alloc创建的结构体类型是socket sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot); if (sk == NULL) goto out; ... sock_init_data(sock, sk);//sock初始化 //另一部分初始化,里面有对各个socket连接定时器的初始化 if (sk->sk_prot->init) { err = sk->sk_prot->init(sk); if (err) sk_common_release(sk); } ... } }
(2)sock_map_fd()
这个函数主要有两个部分,一个是创建file文件结构,fd文件描述符,另一部分是将file文件结构和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; //分配file结构体 newfile = sock_alloc_file(sock, flags, NULL); if (likely(!IS_ERR(newfile))) { fd_install(fd, newfile); return fd; } ... }
至此,socket系统调用就结束了,将fd返回用户使用。
综上,socket系统调用的操作:首先在内核生成一个socket_alloc 和tcp_sock类型的对象,其中sock_alloc对象中的socket和tcp_sock对象的sock绑定,sock_alloc对象中的inode和file类型对象绑定。然后将分配的文件描述符fd和file对象关联,最后将这个文件描述符fd返回给用户使用。
经过这一连串操作,用户只要使用fd,内核就能根据这个fd进行网络连接管理的各种操作。
《五》跟踪分析Socket相关系统调用的内核处理函数
本次跟踪分析基于上次构建的MenuOS系统, 通过gdb设置断点来跟踪分析socket内核处理函数;
在linux-5.0.1目录下打开新的终端,执行命令,如下:
gdb file vmlinux target remote:1234 b __sys_socket b __sys_bind b __sys_listen b __sys_shutdown
通过以上指令,系统会进入gdb模式,通过file vmlinux加载vmlinux,用target remote:1234和menuos连接, 后面指令为设置的端点,执行到对应函数时,程序暂停,按c执行下去。结果显示如下,可见在replyhi 执行过程中,调用了socket()、bind()、listen()等API函数 ,实验完成。