共计 3104 个字符,预计需要花费 8 分钟才能阅读完成。
Linux 网络编程 API 函数初步分析
明天咱们来剖析一下前几篇博文中提到的网络编程中几个外围的 API,探索一下当咱们调用每个 API 时,内核中具体做了哪些筹备和初始化工作。
1、socket(family,type,protocol)
当咱们在开发网络应用程序时,应用该零碎调用来创立一个套接字。该 API 所做的工作如下所示:
该零碎调用次要实现两个工作:“创立套接字”和“为套接字绑定文件句柄”。
socket{}<include/linux/net.h> 构造定义如下:
struct socket {
socket_state state; //socket 状态
unsigned long flags; // 标识,如 SOCK_ASYNC_NOSAPCE
const struct proto_ops *ops; // 协定特定的 socket 操作集
struct fasync_struct *fasync_list; // 异步唤醒队列
struct file *file; // 指向文件的指针
struct sock *sk; // 指向下一层中的 sock 构造
wait_queue_head_t wait; // 期待在这个 socket 上的工作列表
short type; // 数据包的类型
};
在创立 socket 套接字时,就是要实现 ops、file 和 sk 等这些成员的初始化。
1). 创立套接字:sock_create()
依据 family 参数值在全局数组 struct net_proto_family net_families[] 里找到咱们所指定的地址簇。不同类型的地址簇都有一个 struct net_proto_family{}类型的对象,例如咱们常见的 IPv4 的 inet_family_ops,IPv6 的 inet6_family_ops,X25 协定的 ax25_family_ops 等。在内核是初始化时,这些模块会在本人的初始化函数外部调用 sock_register()接口将各自的地址簇对象注册到 net_families[] 数组里。
咱们剖析的焦点集中在 IPv4 协定簇,即 inet_family_ops 对象上。重点是 inet_create 函数,该函数的次要工作就是创立一个 socket 套接字,并对其中相干构造体成员进行必要的初始化。至于它创立套接字时的根据和原理等到咱们讲协定栈时大家就明确了,这里次要是让大家对其流程执行流程有个理性的把握。
sock_alloc()函数中咱们创立一个 struct socket{}类型的对象,如果叫做 A,将 socket()零碎调用的第二参数 type 字段赋值给 A ->type。
在 inet_create()函数中,咱们依据 type 的值,在全局数组 struct inet_protosw inetsw[]里找到咱们对应的协定转换开关。而 inetsw[]数组是在 inet_init()函数里被初始化的:
其中 inetsw_array[]是一个比拟重要的数据结构,定义在 af_inet.c 文件中:
依据 type 的值,就能够确定 struct socket{}->ops,到底是 inet_stream_ops、inet_dgram_ops 或者 inet_sockraw_ops。而后,对应地,就以 tcp_prot、udp_prot 或 raw_prot 为输出参数,实例化一个 struct sock{}对象 sk=sk_alloc()。紧接着建设 socket{}和 sock{}的关联,最初将 socket()零碎调用的第三个参数 protocol 付给 sock{}对象中的属性 sk_protocol。
看不懂别着急,我说过,这里只是给大家梳理整体流程,等到咱们讲了协定栈章节,而后再回头看本篇,就感觉这些货色就太小儿科了。
2). 为套接字绑定文件句柄:sock_map_fd()
咱们都晓得网络套接字也是一种零碎 IO,所以不可避免的要与文件系统打交道。每个套接字都对应一个已关上的文件标识符,所以在套接字初始化实现后,就要将其和本地一个惟一的文件标识符关联起来,即建设 socket{}和 file{}之间的关联关系。
2、bind (sockfd, sockaddr, addrlen)
该零碎调用在内核中的执行过程如下:
重点是 socket->ops->bind()回调接口。咱们当初曾经晓得了,针对 IPv4 而言,这里的 ops 无非就是 inet_stream_ops、inet_dgram_ops 或inet_sockraw_ops对象。碰巧的是,这三个对象中的 bind 函数指针均指向 inet_bind()函数。只有原始套接字的状况,这里会去调用 raw_prot 对象的 bind 回调函数,即 raw_bind()。
3、listen(sockfd, backlog)
这里咱们能够看到面向无连贯的套接字和原始套接字是不必 listen 的,只有流式套接字才无效。
4、connect(sockfd, sockaddr, addrlen)
从这幅图中咱们的确看到,connect()零碎调用岂但能够面向连贯的套接字,也可用于无连贯及原始套接字。
5、accept(sockfd, sockaddr, addrlen)
同样地,咱们看到只有面向连贯的流式套接字调用 accept()才有意义。最终调用的是 tcp_prot 对象的 accept 成员函数。
须要 C /C++ Linux 高级服务器架构师学习材料加群 812855908(包含 C /C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等)
Linux 网络编程数据收发的 API 流程剖析
只有把数据在协定栈中的流动线路和脉络弄清楚了,对于协定栈的实现局部,了解起来就轻松多了。在网络编程章节的数据接管过程中,咱们次要介绍过 read()、recv()、recvfrom()还有一个 recvmsg()没介绍到,明天咱们就来看一下这几个 API 函数到底有什么差异。
数据接管
在接收数据的过程,次要分两个阶段:BOTTOM-HALF和TOP-HALF。BOTTOM-HALF:
当从网卡驱动收到数据包后即进入 BOTTOM-HALF 阶段,在这里要依据以太帧头部中的类型字段来确定下层承载的具体协定类型,如 IP,或 ARP、RARP 等。IP 报文的处理函数通常交付给 ip_recv()函数来解决,而后数据进入网络层,具体流程:如果该数据包是发给本机的个别调用 ip_local_deliver()函数,如果是须要本机转发给进来的,并且本机也开启了转发性能,那么就会调用 ip_forward()函数。在这里咱们看到了 Netfilter 的身影,好久没看到它了,还是有些亲切。大家能够联合这幅图回头再了解一下 Netfilter 和协定栈的关系。BOTTOM-HALF 最初将收到的 skb 填充到 socket 套接字的接管队列里,参见下图。
TOP-HALF:
紧承 BOTTOM-HALF 阶段,该阶段的次要工作就是从接管队列里拿出一个 skb 而后将其传递到用户空间去,如下:
能够看出,这几个函数的外部最终都对立到了一起:__sock_recvmsg()。
数据发送
同样的,数据发送也分两个阶段,对照接管的状况,发送数据时必定也存在一个发送队列,这样想就对了。后面对于发送数据包时咱们介绍过的 API 有 write()、send()、sendto()还有一个 sendmsg()没介绍到。TOP-HALF如下:
BOTTOM-HALF如下所示:
通过这么一份摸索,咱们对这几个数据收发的 API 至多了解的要比他人粗浅些了吧。