linux内核网络子系统初探2—socket层
一、内核网络socket层相关
接着上文,从这章开始,将按照五层网络模型的顺序逐层分析内核代码。
linux1.0网络协议栈部分代码如下:
[root@localhost linux-1.0]# ls net/
ddi.c inet Makefile socket.c Space.c unix
[root@localhost linux-1.0]# ls net/inet/
arp.c dev.h icmp.h loopback.c protocol.c README skbuff.h tcp.h utils.c
arp.h eth.c inet.h Makefile protocol.h route.c sock.c timer.c
datagram.c eth.h ip.c packet.c raw.c route.h sock.h udp.c
dev.c icmp.c ip.h proc.c raw.h skbuff.c tcp.c udp.h
[root@localhost linux-1.0]# ls net/unix/
Makefile proc.c sock.c unix.h
socket部分主要看net/inet/sock.c、net/inet/protocol.c、net/socket.c这几个文件。
我个人的理解,在严格意义上,应用层指的应该是各类核外网络通信模块(例如ftp、http等),socket应该是介于应用层与内核间的接口层,本质不属于应用层。但按功能来讲它是属于应用层的核内接口部分。
二、以用户态API socket()为例,观察系统调用进入内核后的流程
用户态程序调用socket(struct proc *p, struct socket_args *uap, int retval)等系列API后,会调用对应的系统调用函数,从而切换进内核态。
系统调用函数接口定义见下,sys_socketcall包含了socket的所有系统调用API,通过传参确定具体的操作。以用户态API socket()为例,它的调用流程是:切入内核态->进入sys_socketcall的SYS_SOCKET分支->sock_socket()->pops[i]的create():
asmlinkage int
sys_socketcall(int call, unsigned long *args)
{
int er;
switch(call) {
case SYS_SOCKET:
er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
if(er)
return er;
return(sock_socket(get_fs_long(args+0), //这里调用相应的sock_*函数,仅仅传递参数args,不做额外操作
get_fs_long(args+1),
get_fs_long(args+2)));
case SYS_BIND:
er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
if(er)
return er;
return(sock_bind(get_fs_long(args+0),
(struct sockaddr *)get_fs_long(args+1),
get_fs_long(args+2)));
case SYS_CONNECT:
er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
if(er)
return er;
return(sock_connect(get_fs_long(args+0),
(struct sockaddr *)get_fs_long(args+1),
get_fs_long(args+2)));
... ...
}
}
sock_socket函数:
sock_socket(int family, int type, int protocol)
{
int i, fd;
struct socket *sock;
struct proto_ops *ops;
for (i = 0; i < NPROTO; ++i) { //遍历pops数组,匹配family值
if (pops[i] == NULL) continue;//全局pops指针数组是通过sock_register注册的
if (pops[i]->family == family) break;// pops全局定义:static struct proto_ops *pops[NPROTO];
}
if (i == NPROTO) {
DPRINTF((net_debug, "NET: sock_socket: family not found\n"));
return(-EINVAL);
}
ops = pops[i];
... ...
if (!(sock = sock_alloc(1))) {//申请一个struct socket的空间
printk("sock_socket: no more sockets\n");
return(-EAGAIN);
}
sock->type = type;
sock->ops = ops;//将上面通过family值匹配到的ops赋值给struct socket的ops指针对象
if ((i = sock->ops->create(sock, protocol)) < 0) {//这里会调用ops->create,即inet_create
sock_release(sock);
return(i);
}
// #define SOCK_INODE(S) ((S)->inode)
if ((fd = get_fd(SOCK_INODE(sock))) < 0) {//获取当前struct socket的inode指针地址,inode在sock_alloc里创建并初始化。sock_alloc中通过get_empty_inode函数获取文件系统中的一个inode对象
sock_release(sock);
return(-EINVAL);
}
return(fd);//实际返回用户态的值是内核中struct socket里指针对象inode的地址
}
这里补充下inode的相关知识:https://blog.csdn.net/smilejiasmile/article/details/121162741
文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。
操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。“块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。
文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点”。
每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。
inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。
pops数组成员是被sock_register初始化的,简化代码如下:
{
for(i = 0; i < NPROTO; i++) {
if (pops[i] != NULL) continue;//找到pops数组里第一个为空的对象,往里写入ops
pops[i] = ops;
pops[i]->family = family;
return(i);
}
return(-ENOMEM);
}
在这里先简单总结下:socket层有个全局数组static struct proto_ops pops[NPROTO];,在inet_proto_init()函数内会调用sock_register()将static struct proto_ops inet_proto_ops*注册到这个pops数组里。之后,当用户态有API调用时,socket层就可以遍历pops数组,根据每个数组对象的family值来筛选出正确的struct proto_ops对象,从而获得相应的ops操作函数。在上文例子里,就是遍历pops数组筛选出inet_proto_ops,从而获得inet_create函数。
接下来看下inet_proto_init(inet模块初始化函数),简化代码如下:
{
(void) sock_register(inet_proto_ops.family, &inet_proto_ops);//这里会把inet_proto_ops注册到pops全局数组里,一共两处调用sock_register注册pops成员,另一处是unix代码内
/* Add all the protocols. */
for(i = 0; i < SOCK_ARRAY_SIZE; i++) {//这里的tcp_prot、udp_prot是全局变量,分别定义在tcp.c与udp.c。里面定义的是具体传输层协议的操作函数集合
tcp_prot.sock_array[i] = NULL;//这里初始化下这几个struct proto *_prot全局变量
udp_prot.sock_array[i] = NULL;//所有使用*协议的socket连接在内核里的struct sock结构,会被插到*_prot里的sock_array链表里
raw_prot.sock_array[i] = NULL;
}
printk("IP Protocols: ");
for(p = inet_protocol_base; p != NULL;) {//struct inet_protocol *inet_protocol_base全局变量是个链表头,存放各个协议的struct inet_protocol
struct inet_protocol *tmp;
tmp = (struct inet_protocol *) p->next;
inet_add_protocol(p);//这里会把遍历到的struct inet_protocol对象添加到一个全局数组里struct inet_protocol *inet_protos[]
printk("%s%s",p->name,tmp?", ":"\n");
p = tmp;
}
... ...
}
udp_prot、tcp_prot、raw_prot的sock_array是socket层用来存储记录对应协议的连接信息的位置,put_sock和get_sock是用于添加、获取sock_array链表里成员的操作函数。这样做便于socket层管理socket连接。
inet_protocol_base链表的成员:
static struct inet_protocol udp_protocol = {
udp_rcv, /* UDP handler */
NULL, /* Will be UDP fraglist handler */
udp_err, /* UDP error control */
&tcp_protocol, /* next */
IPPROTO_UDP, /* protocol ID */
0, /* copy */
NULL, /* data */
"UDP" /* name */
};
static struct inet_protocol icmp_protocol = {
icmp_rcv, /* ICMP handler */
NULL, /* ICMP never fragments anyway */
NULL, /* ICMP error control */
&udp_protocol, /* next */
IPPROTO_ICMP, /* protocol ID */
0, /* copy */
NULL, /* data */
"ICMP" /* name */
};
struct inet_protocol *inet_protocol_base = &icmp_protocol;
struct inet_protocol *inet_protos[MAX_INET_PROTOS] = {
NULL
};
各个协议的inet_protocol结构体间通过next指针连起来,协议族的链表头是inet_protocol_base。所有结构体通过inet_add_protocol添加到inet_protos数组里,后续会通过inet_protos来获取不同协议的struct inet_protocol对象。这里主要是便于后续ip层获取到tcp、udp等不同协议的接收接口。
socket层的pops与udp_prot/tcp_prot均是为了便于管理,但是针对的地方不同。udp_prot/tcp_prot的存在,是为了便于操作不同协议类型里socket连接的struct sock;pops的存在,是为了便于查找到inet部分操作函数集,从而向下传递数据。
至此,用户API socket()的核内调用流程为:
sys_socketcall->sock_socket()-> pops[i]的create() ->inet_create()
syscall进入内核后层层向下调用的流程已经清晰了,其他的用户态API的系统调用也是按照这个顺序向下传递的,在此不再展开。进一步看下socket层几个核心结构体间的关联。
三、观察内核网络socket层核心结构体
先看下内核socket层关键的结构体定义:
-
socket结构体,里面比较关键的是成员struct proto_ops *ops和成员void *data.
-
sock结构体,里面比较关键的是成员struct proto *prot,成员prot->sock_array数组指针,以及成员struct socket *socket.
socket对象的初始化是在函数sock_socket内实现的,由系统调用sys_socketcall里的SYS_SOCKET。sock_socket伪代码:
sock_socket(int family, int type, int protocol){
struct socket *sock;
struct proto_ops *ops;
ops = 根据传参family匹配到pops中的指定对象
sock = sock_alloc(1);//分配空间给struct *socket指针,初始化部分成员变量
sock->type = type;//赋值传参
sock->ops = ops;//这里应该匹配到上文inet_proto_init里传给sock_register的参数inet_proto_ops
sock->ops->create(sock,protocol);//将struct *socket指针传参,调用inet_create函数,inet_create内创建配置关联struct sock
创建socket结构体的file descriptor;
}
inet_create函数的部分代码见下图:
在inet部分,将网络抽象成了6种类型:
- SOCK_STREAM,面向字节流(tcp),工作在传输层
- SOCK_DGRAM,面向数据报(udp),工作在传输层
- SOCK_RAW,原始套接字(可以处理ICMP、IGMP等网络报文、特殊的IPv4报文、可以通过IP_HDRINCL套接字选项由用户构造IP头),工作在网络层。
- SOCK_RDM,一种可靠的UDP形式(保证交付数据报但不保证顺序)
- SOCK_SEQPACKET,可靠的连续数据包服务
- SOCK_PACKET,建立套接字的时候选择SOCK_PACKET类型,表示截取的数据帧在物理层,内核将不对网络数据进行处理而直接交给用户,即数据直接从网卡的协议栈交给用户。高版本内核仍然支持,但此功能比较过时,很少使用。
在inet_create里,会根据传参struct socket对象的type值,选择进入上面抽象出的不同分支,然后配置相应的sock结构体成员prot。
inet_create伪代码:
inet_create(struct socket *sock, int protocol){
struct sock *sk;
struct proto *prot;
sk = kmalloc();//申请一个sock结构体空间
配置sk的属性;
sk->num = 0;
switch(sock->type){//不同的SOCK_*网络类型
case SOCK_STREAM://面向字节流
case SOCK_SEQPACKET: //可靠的连续数据包服务
传入参数protocol异常情况处理;
protocol = IPPROTO_TCP;
sk->no_check = TCP_NO_CHECK;
prot = &tcp_prot;
break;
case SOCK_DGRAM:
传入参数protocol异常情况处理;
protocol = IPPROTO_UDP;
sk->no_check = UDP_NO_CHECK;
prot=&udp_prot;
break;
case SOCK_RAW:
传入参数protocol异常情况处理;
prot = &raw_prot;
sk->reuse = 1;
sk->no_check = 0;
sk->num = protocol;//在SOCK_RAW与SOCK_PACKET情况下,会配置sock成员num值
break;
case SOCK_PACKET:
传入参数protocol异常情况处理;
prot = &packet_prot;
sk->reuse = 1;
sk->no_check = 0;
sk->num = protocol;//在SOCK_RAW与SOCK_PACKET情况下,会配置sock成员num值
break;
default:
... ...
}
sk->socket = sock;//将sock结构体中的成员socket指针指向当前传进来的socket对象
sk->type = sock->type;//将sock结构体与socket结构体中的type配成一样的值。
sk->protocol = protocol;//例如IPPROTO_TCP
sk->prot = prot;//例如tcp_prot
sock->data =(void *) ·sk;//将socket结构体的成员指针data指向sock结构体
... ...//配置sk->属性
put_sock(sk->num, sk);//调用put_sock将当前sock对象挂到sock->prot->sock_array链表里便于管理,sock_array是个哈希链表
... ...
}
put_sock功能是将sock对象添加到对应协议的成员sock_array哈希链表中,伪代码如下,关键代码如图示:
void put_sock(unsigned short num, struct sock *sk){
struct sock *sk1;
struct sock *sk2;
int mask;
sk->num = num;
sk->next = NULL;
num = num &(SOCK_ARRAY_SIZE - 1);//用num(sk->num)做hash表的key
if(sk->prot->sock_array[num] == NULL){ //如果hash表里key位置为空,说明当前sk是队列第一个节点
sk->prot->sock_array[num] = sk;
return;
}
// mask为0xff000000 => 0xffff0000 => 0xffffff00 => 0xffffffff
for(mask = 0xff000000; mask != 0xffffffff; mask = (mask >> 8) | mask) {
if ((mask & sk->saddr) && (mask & sk->saddr) != (mask & 0xffffffff)) {
mask = mask << 8;
break;
}
}
sk1 = sk->prot->sock_array[num];//sk1指向哈希表对应key的链表头节点
for(sk2 = sk1; sk2 != NULL; sk2=sk2->next) {//sk2、sk1做遍历指针,sk1代表已遍历过的最后一个原链表对象,sk2代表待检查位置
if (!(sk2->saddr & mask)) {//当前sk2指针对应的位置没有ip地址,即sk2可插入元素,将插在sk2前。将空元素放到最后
if (sk2 == sk1) {//代表链表为空,头插法
sk->next = sk->prot->sock_array[num];//将待插入sock的next指针指向 链表头(此时链表头为空)
sk->prot->sock_array[num] = sk;//链表头节点替换成当前的sock
return;//插入成功退出函数
}
sk->next = sk2;//将待插入sock的next指针指向sk2,即插在sk2前
sk1->next= sk;//原链表的最后一个元素的next指针指向待插入sock
return;//插入成功退出函数
}
sk1 = sk2;//当前位置不为空,不可插入,向后遍历,sk1移到当前遍历过的最后一个原链表元素
}
sk->next = NULL;
sk1->next = sk;
}
在这里总结一下上面列出的关键结构体间关系图:
四、观察内核网络socket层收发数据的调用路径:
用户态里,udp协议常用的发送接收编程API是sendto()、recvfrom(),tcp协议常用的发送接收API是send()、recv()。
它们的调用流程分别为:
- sys_socketcall中SYS_SENDTO分支->sock_sendto->inet_sendto-> (sk->prot->sendto)
- sys_socketcall中SYS_RECVFROM分支->sock_recvfrom->inet_recvfrom->(sk->prot->recvfrom)
- sys_socketcall中SYS_SEND分支->sock_send->inet_send->(sk->prot->write)
- sys_socketcall中SYS_RECV分支->sock_recv->inet_recv->(sk->prot->read)
这几个函数内部操作基本一致:
如果当前sock未指定端口,则内核调get_new_socknum函数获取一个空闲的端口号分配给sock用于数据传输。具体功能通过调用sock结构体里的->prot里相应的函数实现。这里的prot就对应的是初始化sock结构体时配置的tcp_prot等proto结构体指针,由此将数据传到了tcp+udp层(传输层)。
五、 小结
linux v1.0版本里,可以看出以下的层次关系:
在高版本的linux内核里,网络子系统进一步细化为BSD socket层、INET socket层、传输层、IP层、数据链路层这五层(见下图)。主要是新封装了一些socket相关函数作为BSD socket接口层单独放到socket.c文件里,并将sock.c文件中的inet操作部分提炼成INET socket接口层,单独放到af_inet.c文件中。高版本内核sock结构工作在INET socket层,socket结构工作在BSD socket层,所有BSD socket层的操作都通过struct sock及其域字段prot指针转化为相应的协议函数处理,所以sock结构是维系BSD socket层和INET socket层的纽带。
其中:
BSD(Berkeley Software Distribution)socket:提供向上统一的 SOCKET 操作接口,核心结构体是struct socket。
INET(指一切支持 IP 协议的网络) socket:INET socket 层,实现BSD的具体接口功能,向传输层传递请求与数据,核心结构体是struct sock。
对于内核socket层而言,主要是通过关键结构体struct socket与struct sock来实现向tcp/udp层传递数据的功能,并不会对数据本身做处理。
附录:socket层的数据分流功能
数据传输过程中,socket层有个至关重要的功能就是数据分流,即为到达的数据筛选出一条正确的传输路径,传递给下一层。观察v1.0 socket层代码,可以发现在创建socket连接核内相关结构时,在inet_create里,会给每个socket连接,配置好传输层对应协议的ops,即筛选出该socket连接上后续数据向下传递的路径。后续数据到达socket层后,就可以直接调用传输层接口向下传递了。
高版本内核里,socket层是否也是在inet socket层里做了相同的工作呢?
以下是高版本4.19.90内核,以sendto() API为例,观察它在socket内部的调用流程:
SYSCALL_DEFINE6(sendto, ... ...) //socket.c,BSD socket层
{
return __sys_sendto(fd, buff, len, flags, addr, addr_len);
}
int __sys_sendto(int fd, ... ...) //socket.c,BSD socket层
{
struct socket *sock;
... ...
err = sock_sendmsg(sock, &msg);
... ...
}
int sock_sendmsg(struct socket *sock, struct msghdr *msg) //socket.c,BSD socket层
{
int err = security_socket_sendmsg(sock, msg, msg_data_left(msg));
return err ?: sock_sendmsg_nosec(sock, msg);
}
EXPORT_SYMBOL(sock_sendmsg);
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg) //socket.c,BSD socket层
{
int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));//这里struct socket的ops,定义:const struct proto_ops *ops;,这里调用的是传输层ops的API
BUG_ON(ret == -EIOCBQUEUED);
return ret;
}
来看下4.19.90 里socket的创建过程:
static int __init inet_init(void) //af_inet.c, INET socket层
{
... ...
(void)sock_register(&inet_family_ops);//inet_family_ops定义如下,sock_register将inet_family_ops注册到net_families数组里,后续有用户态socket相关请求时会去查询net_families找到匹配项
... ...
for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)//定义:static struct list_head inetsw[SOCK_MAX];
INIT_LIST_HEAD(r);
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);//将inetsw_array里的元素注册到inetsw链表里
/*
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
... ...
};
*/
... ...
}
int sock_create(int family, int type, int protocol, struct socket **res) //构造struct socket的函数,socket.c, BSD socket层
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
EXPORT_SYMBOL(sock_create);
int __sock_create(struct net *net, int family, ... ...) //socket.c, BSD socket层
{
struct socket *sock;
... ...
pf = rcu_dereference(net_families[family]); //找到inet_family_ops
... ...
err = pf->create(net, sock, protocol, kern);//调的是inet_create
... ...
}
static int inet_create(struct net *net, struct socket *sock, ... ...) //af_inet.c, INET socket层
{
struct sock *sk;
struct inet_protosw *answer;
struct proto *answer_prot;
... ...
lookup_protocol:
err = -ESOCKTNOSUPPORT;
rcu_read_lock();
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {//遍历sock->type类型的成员
err = 0;
if (protocol == answer->protocol) { //匹配到合适的protocol就退出循环
if (protocol != IPPROTO_IP)//在inetsw里最后一个成员是IPPROTO_IP类型。
break;
/*如果遍历到最后一个成员,说明前面都没匹配成功,即protocol就是IPPROTO_IP。
进入了"protocol == answer->protocol"的分支后,不会进入判断
"protocol != IPPROTO_IP"里,进而不会break,而是会将err置为-EPROTONOSUPPORT。
由于是最后一个成员,遍历过后会退出循环,进而对err的异常值进行处理。
*/
} else {
if (IPPROTO_IP == protocol) {//sock->type是SOCK_RAW, protocol是IPPROTO_IP,则返回成员IPPROTO_IP
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
/*如果进了“IPPROTO_IP == answer->protocol”这个分支,
说明已经遍历到了最后一个成员
*/
}
err = -EPROTONOSUPPORT;
}
... ...
sock->ops = answer->ops; //这里会匹配到下一层传输层的操作函数
... ...
}
所以实际上,在高版本内核里,socket层的逻辑依然是:
- 在创建socket连接相关结构时,在INET socket层会为该条连接配置好向下层传输的路径,进行数据分流
- 在数据到达socket层时,在BSD socket层可直接调用已配好的接口,向下传递数据。
最后总结下:
在每条socket连接创建时,INET socket层会为该条连接筛选出向下层传输数据的路径。后续该条连接上的数据到达内核后,在BSD socket层会直接调用已配好的接口,向传输层传递。所以不太好简单的概括为,INET socket层实现了数据分流,还是要BSD和INET一块作为socket层来看它的功能,二者合作完成了分流的功能。