在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)
{intretval;struct socket *sock;intflags;
...
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)gotoout_release;out:/*It may be already another descriptor 8) Not kernel problem.*/
returnretval;
out_release:
sock_release(sock);returnretval;
}
下面主要分析sock_create和sock_map_fd这两个函数,看它们是怎么在内核一步步创建和管理我们使用的socket。
(1)sock_create函数
sock_create() 实际调用的是 __sock_create()。
int __sock_create(struct net *net, int family, int type, intprotocol,struct socket **res, intkern)
{interr;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)gotoout_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, intprotocol,intkern)
{struct sock *sk;struct inet_protosw *answer;struct inet_sock *inet;struct proto *answer_prot;
unsignedcharanswer_flags;charanswer_no_check;int try_loading_module = 0;interr;
...
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, intflags)
{struct file *newfile;//获取一个未使用的文件描述符,文件描述符的管理这里就暂不分析了
int fd =get_unused_fd_flags(flags);if (unlikely(fd < 0))returnfd;//分配file结构体
newfile =sock_alloc_file(sock, flags, NULL);if (likely(!IS_ERR(newfile))) {
fd_install(fd, newfile);returnfd;
}
...
}
至此,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:1234b __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函数,实验完成。