Linux如何实现网络通信
网络IO模型
同步和异步
关注的是调用方是否主动获取结果
同步:调用方主动等待结果的返回
异步:调用法不需要等待,而是通过别的方法,比如:回调函数,状态通知等。
阻塞和非阻塞
关注等待结果返回之前调用方的状态
阻塞:结果返回之前,当前线程被挂起
非阻塞:结果返回之前,线程可以做一些别的事情,不会被挂起
五种I/O模型
Linux内核的网络通信
在Linux的源代码中,网络设备驱动对应的逻辑位于driver/net/ethernet, 其中intel系列网卡的驱动在driver/net/ethernet/intel目录下。协议栈模块代码位于kernel和net目录。
其中net目录中包含Linux内核的网络协议栈的代码。子目录 ipv4和ipv6为TCP/IP 协议栈的IPv4和 IPv6 的实现,主要包含了TCP、UDP、IP协议的代码,还有ARP 协议、ICMP 协议、IGMP 协议代码实现,以及如proc、ioctl等控制相关的代码。
网络协议栈是由若干个层组成的,网络数据的流程主要是指在协议栈的各个层之间的传递。一个TCP服务器的流程按照建立socket()函数,绑定地址端口 bind()函数,侦听端口 listen()函数,接收连接accept()函数,发送数据send()函数,接收数据recv()函数,关闭socket()函数的顺序来进行。
与此对应内核的处理过程也是按照此顺序进行的,网络数据在内核中的处理过程主要是在网卡和协议栈之间进行:从网卡接收数据,交给协议栈处理;协议栈将需要发送的数据通过网络发出去。
由下图中可以看出,数据的流向主要有两种。应用层输出数据时,数据按照自上而下的顺序,依次通过应用API层、协议层 和接口层;当有数据到达的时候,自下而上依次通过接口层、协议层和应用API层的方式,在内核层传递。
应用层Socket的初始化、绑定(bind)和销毁是通过调用内核层的socket()函数进行资源的申请和销毁的。
发送数据的时候,将数据由应用API层传递给协议层,协议层在UDP层添加UDP的首部、TCP层添加TCP的首部、IP层添加IP的首部,接口层的网卡则添加以太网相关的信息后,通过网卡的发送程序发送到网络上。
接收数据的过程是一个相反的过程,当有数据到来的时候,网卡的中断处理程序将数据从以太网网卡的FIFO对列中接收到内核,传递给协议层,协议层在IP层剥离IP的首部、UDP层剥离UDP的首部、TCP层剥离TCP的首部后传递给应用API层,应用API层查询socket 的标识后,将数据送给用户层匹配的socket。
在Linux内核实现中,链路层协议靠网卡驱动来实现,内核协议栈来实现网络层和传输层。内核对更上层的应用层提供socket接口来供用户进程访问。
Linux下的IO复用
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,并等待读写完成。
文件描述符FD
在Linux操作系统中,可以将一切都看作是文件,包括普通文件,目录文件,字符设备文件(如键盘,鼠标…),块设备文件(如硬盘,光驱…),套接字等等,所有一切均抽象成文件,提供了统一的接口,方便应用程序调用。
在Linux操作系统中,为了将应用程序和打开的文件对应上,产生了文件描述符FD。
文件描述符:File descriptor,简称fd,当应用程序请求内核打开/新建一个文件时,内核会返回一个文件描述符用于对应这个打开/新建的文件,其fd本质上就是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
系统为了维护文件描述符建立了3个表:进程级的文件描述符表、系统级的文件描述符表、文件系统的i-node表。所谓进程级的文件描述符表,指操作系统为每一个进程维护了一个文件描述符表,该表的索引值都从从0开始的,所以在不同的进程中可以看到相同的文件描述符,这种情况下相同的文件描述符可能指向同一个实际文件,也可能指向不同的实际文件。
select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。
int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。当创