Linux网络编程API函数初步剖析linux
今天咱们来分析一下前几篇博文中提到的网络编程中几个核心的API,探究一下当咱们调用每一个API时,内核中具体作了哪些准备和初始化工做。golang
一、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套接字,并对其中相关结构体成员进行必要的初始化。至于它建立套接字时的依据和原理等到咱们讲协议栈时你们就明白了,这里主要是让你们对其流程执行流程有个感性的把握。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{}之间的关联关系。
二、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()。
三、listen(sockfd, backlog)
这里咱们能够看到面向无链接的套接字和原始套接字是不用listen的,只有流式套接字才有效。
四、connect(sockfd, sockaddr, addrlen)
从这幅图中咱们确实看到,connect()系统调用不但能够面向链接的套接字,也可用于无链接及原始套接字。
五、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至少理解的要比别人深入些了吧。