内核与协议无关接口层的实现

时间: 2024-11-10 admin IT培训

内核与协议无关接口层的实现

内核与协议无关接口层的实现

主要参考了《深入linux内核架构》和《精通Linux内核网络》相关章节

文章目录

  • 与协议无关接口层(传输层和用户空间的接口)
    • 套接字对应的实例
      • socket
      • sock
    • 套接字和文件
    • socketcall系统调用
    • 创建套接字
      • inet_create
    • 接收数据
    • 发送数据

与协议无关接口层(传输层和用户空间的接口)

套接字将UNIX隐喻“万物皆文件”应用到了网络连接上。内核与用户空间套接字之间的接口实现在C标准库中,使用了socketcall系统调用。
socketcall充当一个多路分解器,将各种任务分配由不同的过程执行,例如打开一个套接字、绑定或发送数据。

Linux采用了内核套接字的概念,使得与用户空间中的套接字的通信尽可能简单。对程序使用的每个套接字来说,都对应于一个socket结构和sock结构的实例。

套接字对应的实例

socket

二者分别充当向下(到内核)和向上的(到用户空间)接口。

struct socket {socket_state		state; // 状态kmemcheck_bitfield_begin(type); // 调试short			type; // 所用协议类型的数字标识符kmemcheck_bitfield_end(type);unsigned long		flags;struct socket_wq __rcu	*wq;//  file是一个指针,指向一个伪文件的file实例,用于与套接字通信(前文讨论过,用户应用程序使用普通的文件描述符进行网络操作)struct file		*file;// 结构中包含的sock指针,指向一个更为冗长的结构,包含了对内核有意义的附加的套接字管理数据。struct sock		*sk;// 用proto_ops指针指向一个数据结构,其中包含用于处理套接字的特定于协议的函数,C库函数会通过socketcall系统调用导向上述的函数指针// 如connect、bind、listenconst struct proto_ops	*ops;
};

sock

请注意,内核自身将最重要的一些成员放置到sock_common结构中,并将该结构的一个实例嵌入到struct sock开始处。

struct sock {struct sock_common	__sk_common;struct sk_buff_head     sk_receive_queue; // 一个存储入站的数据包队列int                     sk_rcvbuf; // 接收缓冲区大小unsigned long           sk_flags; // 各种标志,如SOCK_DEAD或SOCK_DBGint                     sk_sndbuf; // 发送缓冲区大小struct sk_buff_head     sk_write_queue; // 存储出站数据包队列. . .unsigned int            sk_shutdown  : 2,sk_no_check  : 2, // 禁用校验和标志sk_protocol  : 8, // 协议标识符,为socket系统调用的第3个参数sk_type      : 16; // 套接字类型,如SOCK_STREAM. . .void                    (*sk_data_ready)(struct sock *sk, int bytes); // 通知套接字有新数据到达void                    (*sk_write_space)(struct sock *sk); // 用于指出可用来处理数据传输的内存
};
  • 系统的各个sock结构实例被组织到一个协议相关的散列表中。skc_node用作散列表的表元,而skc_hash表示散列值。
  • 在发送和接收数据时,需要将数据放置在包含套接字缓冲区的等待队列上(sk_receive_queue和sk_write_queue)。
  • 此外,每个sock结构都关联了一组回调函数函数,由内核用来引起用户程序对特定事件的关注或进行状态改变。在我们给出的简化版本中,只有一个函数指针sk_data_ready,因为它是最重要的,而且在前几节中已经提到几次。在数据到达后,需要用户进程处理时,将调用该指针指向的函数。通常,指针的值是sock_def_readable。

socket结构的ops成员类型为struct proto_ops,而sock的prot成员类型为struct proto,二者很容易混淆。

这两个结构中有些成员的名称相似(经常是相同的),但它们表示不同的功能。sock给出的操作用于(内核端)套接字层和传输层之间的通信,而socket结构的ops成员所包含的各个函数指针则用于与系统调用通信。换句话说,它们构成了用户端和内核端套接字之间的关联。

套接字和文件

在连接建立后,用户空间进程使用普通的文件操作来访问套接字。这在内核中是如何实现的呢?
由于VFS层的开放结构,只需要很少的工作。

每个套接字都分配了一个该类型的inode,inode又关联到另一个与普通文件相关的结构。用于操作文件的函数保存在一个单独的指针表中

因而,对套接字文件描述符的文件操作,可以透明地重定向到网络子系统的代码。套接字使用的
file_operations如下

struct file_operations socket_file_ops = { .owner =  THIS_MODULE, .llseek =  no_llseek, .aio_read =  sock_aio_read, .aio_write =  sock_aio_write, .poll =  sock_poll, .unlocked_ioctl = sock_ioctl, pat_ioctl = compat_sock_ioctl, .mmap =  sock_mmap, .open =  sock_no_open, /* 专用的open代码,禁止通过/proc打开 */ .release =  sock_close, .fasync =  sock_fasync, .sendpage =  sock_sendpage, .splice_write = generic_splice_sendpage, 
};  

前缀为sock_的函数都是简单的包装器例程,它们会调用sock_operations中的例程,如下例中的sock_mmap所示

static int sock_mmap(struct file * file, struct vm_area_struct * vma)
{  struct socket *sock = file->private_data;  return sock->ops->mmap(file, sock, vma);  
}

inode和套接字的关联,是通过下列辅助结构,将对应的两个结构实例分配到内存中的连续位置:

struct socket_alloc {  struct socket socket;  struct inode vfs_inode;  
};

内核提供了两个宏来进行必要的指针运算,根据inode找到相关的套接字实例(SOCKET_I),或反过来(SOCK_INODE)。为简化处理,每当将一个套接字附加到文件时,sock_attach_fd将struct file的private_data成员设置为指向socket实例。上面给出的sock_mmap例子就利用了这一点。

socketcall系统调用

​ 文件功能中的读写操作可以通过虚拟文件系统相关系统调用进入内核,然后重定向到socket_file_ops结构的函数指针,除此之外,还需要对套接字执行其他任务,这些不能融入到文件方案中。举例来说,这些操作包括创建套接字、bind、listen等。

​ 为此,Linux提供了socketcall系统调用,它实现在sys_socketcall中,17个套接字操作只对应到一个系统调用,这比较引人注目。由于所要处理的任务不同,参数列表可能差别很大。该系统调用的第一个参数是一个数值常数,选择所要的系统调用。例如,可能的值包括SYS_SOCKET、SYS_BIND、SYS_ACCEPT和SYS_RECV。标准库的例程名称与这些常数基本上是一一对应的,但在内部都重定向为使用socketcall和对应的常数。只有一个系统调用,是由历史原因造成
的。

sys_socketcall的任务并不特别困难,它只充当一个分派器,将系统调用转到其他函数并传递参数,后者中的每个函数都实现了一个“小”的系统调用。

创建套接字

sys_socket是创建新套接字的起点。相关的代码流程图在图12-32给出。
首先,使用sock_create创建一个新的套接字数据结构,该函数直接调用了__sock_create。分
配所需内存的任务委托给sock_alloc,该函数不仅为struct socket实例分配了空间,还紧接着该实例为一个inode实例分配了内存空间。正如前文的讨论,这使得两个对象可以联合起来。

内核的所有传输协议都群集在net/socket.c中定义的数组static struct net_proto_family

  • net_families[NPROTO]中(sock_register用于向该数据库增加新数据项)。 各个数组项都提供了特定于协议的初始化函数。
struct net_proto_family {  int family; int  (*create)(struct socket *sock, int protocol);  struct module  *owner;
};

在为套接字分配内存后,刚好调用函数create。**inet_create用于因特网连接(TCP和UDP都使用该函数)。**它创建一个内核内部的sock实例,尽可能初始化它,并将其插入到内核的数据结构。
map_sock_fd为套接字创建一个伪文件(文件操作通过socket_ops指定)。还要分配一个文件描述符,将其作为系统调用的结果返回。

inet_create

static const struct net_proto_family inet_family_ops = {.family = PF_INET,.create = inet_create,.owner	= THIS_MODULE,
};

net\ipv4\af_inet.c

// 为以及分配好内存的socket实例执行初始化操作
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;int try_loading_module = 0;int err;if (protocol < 0 || protocol >= IPPROTO_MAX)return -EINVAL;sock->state = SS_UNCONNECTED; // 状态初始化/* Look for the requested type/protocol pair. */
lookup_protocol:err = -ESOCKTNOSUPPORT;rcu_read_lock();list_for_each_entry_rcu(answer, &inetsw[sock->type], list) { // 找到合适的answer来初始化socketerr = 0;/* Check the non-wild match. */if (protocol == answer->protocol) {if (protocol != IPPROTO_IP)break;} else {/* Check for the two wild cases. */if (IPPROTO_IP == protocol) {protocol = answer->protocol;break;}if (IPPROTO_IP == answer->protocol)break;}err = -EPROTONOSUPPORT;}if (unlikely(err)) {if (try_loading_module < 2) {rcu_read_unlock();/** Be more specific, e.g. net-pf-2-proto-132-type-1* (net-pf-PF_INET-proto-IPPROTO_SCTP-type-SOCK_STREAM)*/if (++try_loading_module == 1)request_module("net-pf-%d-proto-%d-type-%d",PF_INET, protocol, sock->type);/** Fall back to generic, e.g. net-pf-2-proto-132* (net-pf-PF_INET-proto-IPPROTO_SCTP)*/elserequest_module("net-pf-%d-proto-%d",PF_INET, protocol);goto lookup_protocol;} elsegoto out_rcu_unlock;}err = -EPERM;if (sock->type == SOCK_RAW && !kern &&!ns_capable(net->user_ns, CAP_NET_RAW))goto out_rcu_unlock;sock->ops = answer->ops; // 操作集初始化answer_prot = answer->prot;answer_flags = answer->flags;rcu_read_unlock();WARN_ON(!answer_prot->slab);err = -ENOBUFS;sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);if (!sk)goto out;err = 0;if (INET_PROTOSW_REUSE & answer_flags)sk->sk_reuse = SK_CAN_REUSE;inet = inet_sk(sk);inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0;inet->nodefrag = 0;if (SOCK_RAW == sock->type) {inet->inet_num = protocol;if (IPPROTO_RAW == protocol)inet->hdrincl = 1;}if (net->ipv4.sysctl_ip_no_pmtu_disc)inet->pmtudisc = IP_PMTUDISC_DONT;elseinet->pmtudisc = IP_PMTUDISC_WANT;inet->inet_id = 0;sock_init_data(sock, sk);sk->sk_destruct	   = inet_sock_destruct;sk->sk_protocol	   = protocol;sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;inet->uc_ttl	= -1;inet->mc_loop	= 1;inet->mc_ttl	= 1;inet->mc_all	= 1;inet->mc_index	= 0;inet->mc_list	= NULL;inet->rcv_tos	= 0;sk_refcnt_debug_inc(sk);if (inet->inet_num) {/* It assumes that any protocol which allows* the user to assign a number at socket* creation time automatically* shares.*/inet->inet_sport = htons(inet->inet_num);/* Add to protocol hash chains. */err = sk->sk_prot->hash(sk);if (err) {sk_common_release(sk);goto out;}}if (sk->sk_prot->init) {err = sk->sk_prot->init(sk);if (err) {sk_common_release(sk);goto out;}}if (!kern) {err = BPF_CGROUP_RUN_PROG_INET_SOCK(sk);if (err) {sk_common_release(sk);goto out;}}
out:return err;
out_rcu_unlock:rcu_read_unlock();goto out;
}

接收数据

使用recvfrom和recv以及与文件相关的readv和read函数来接收数据。因为这些函数的代码非
常类似,在处理过程的早期就合并起来,因此我们只讨论sys_recvfrom

用于确定目标套接字的文件描述符传递到该系统调用。因此,第一个任务是找到对应的套接字。首先,fget_light根据task_struct的描述符表,查找对应的file实例。sock_from_file确定与之关联的inode,并通过使用SOCKET_I最终找到相关的套接字。
在一些准备工作之后(不在这里讨论),sock_recvmsg调用特定于协议的接收例程sock->ops-> recvmsg。例如,TCP使用tcp_recvmsg来完成该工作。UDP使用的例程是udp_recvmsg。UDP的实现并不特别复杂。

  • 如果接收队列(通过sock结构的receive_queue成员实现)上至少有一个分组,则移除并返
    回该分组。
  • 如果接收队列是空的,显然没有数据可以传递到用户进程。在这种情况下,进程使用
    wait_for_packet使自身睡眠,直至数据到达。

在新数据到达时总是调用sock结构的data_ready函数,因而进程可以在此时被唤醒。

move_addr_to_user将数据从内核空间复制到用户空间,使用了第2章描述的copy_to_user函数。 TCP的实现遵循了类似的模式,但其中涉及许多细节和协议的奇异之处,因而要稍微复杂一些。

发送数据

用户空间程序在发送数据时,还有几种可供选择的方法。它们可以使用两个与网络有关的库函数(sendto和send)或文件层的write和writev函数。同样,这些函数的控制流在内核中的特定位置会合并为一,因此,考察上述第一个函数的实现(在内核源代码的sys_sendto过程中)即足以。

fget_light和sock_from_file根据文件描述符查找相关的套接字。发送的数据使用move_addr_to_kernel从用户空间复制到内核空间,然后sock_sendmsg调用特定于协议的发送例程sock->ops->sendmsg。该例程产生一个所需协议格式的分组,并转发到更低的协议层。