您现在的位置是:首页 >技术教程 >linux内核网络子系统初探2---socket层网站首页技术教程

linux内核网络子系统初探2---socket层

krokodil98 2024-06-17 10:13:24
简介linux内核网络子系统初探2---socket层

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
"));
        return(-EINVAL);
  }
  ops = pops[i];
  ... ...
  if (!(sock = sock_alloc(1))) {//申请一个struct socket的空间
        printk("sock_socket: no more sockets
");
        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?", ":"
");
        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种类型:

  1. SOCK_STREAM,面向字节流(tcp),工作在传输层
  2. SOCK_DGRAM,面向数据报(udp),工作在传输层
  3. SOCK_RAW,原始套接字(可以处理ICMP、IGMP等网络报文、特殊的IPv4报文、可以通过IP_HDRINCL套接字选项由用户构造IP头),工作在网络层。
  4. SOCK_RDM,一种可靠的UDP形式(保证交付数据报但不保证顺序)
  5. SOCK_SEQPACKET,可靠的连续数据包服务
  6. 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层来看它的功能,二者合作完成了分流的功能

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。