目录
Linux代码结构看网络通信
Linux下的IO复用编程
文件描述符FD
select
poll
epoll
select、poll、epoll的比较
1、支持一个进程所能打开的最大连接数
2、FD剧增后带来的IO效率问题
3、消息传递方式
总结
Linux代码结构看网络通信
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下的IO复用编程
select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,并等待读写完成。
文件描述符FD
在 Linux 操作系统中,可以将一切都看作是文件,包括普通文件,目录文件,所有一切均抽象 成文件,提供了统一的接口,方便应用程序调用。
文件描述符: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);
不同与 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针实现。 pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select“参数-值”传递的方式。同时,pollfd 并没有最大数量限制(但是数量过大后性能也是会下降)。和select函数一样,poll返回后,需要轮询 pollfd 来获取就绪的描述符。
epoll
epoll 是在 2.6 内核中提出的,是之前的 select 和 poll 的增强版本。相对于 select 和 poll 来说,可以看到 epoll 做了更细致的分解,包含了三个方法,使用上更加灵活。
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_create(int size);
创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共有多大,这个参数不同于 select()中的第一个参数,给出最大监听的 fd+1 的值,参数 size 并不是限制了epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。当创建好 epoll 句 柄后,它就会占用一个 fd 值,在 linux 下如果查看/proc/进程 id/fd/,是能够看到这个 fd 的, 所以在使用完 epoll 后,必须调用 close()关闭,否则可能导致 fd 被耗尽。 可以理解为对应于JDK NIO 编程里的 selector = Selector.open();
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数是对指定描述符 fd 执行 op 操作。 epfd:是 epoll_create()的返回值。 op:表示 op 操作,用三个宏来表示:添加 EPOLL_CTL_ADD,删除 EPOLL_CTL_DEL,修 改 EPOLL_CTL_MOD。分别添加、删除和修改对 fd 的监听事件。 fd:是需要监听的 fd(文件描述符) epoll_event:是告诉内核需要监听什么事,有具体的宏可以使用,比如 EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭)。EPOLLOUT:表示对应的文件描述符可以写。可以理解为对应于 JDK NIO 编程里的socketChannel.register();
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待 epfd 上的 io 事件,最多返回 maxevents 个事件。参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这 个 maxevents 的值不能大于创建 epoll_create()时的 size,参数 timeout 是超时时间(毫秒,0 会立即返回,-1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目, 如返回 0 表示已超时。可以理解为对应于JDK NIO 编程里的 selector.select();
select、poll、epoll的比较
select,poll,epoll都是操作系统实现 IO 多路复用的机制。通过这种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
三种机制的区别如下:
1、支持一个进程所能打开的最大连接数
select:
单个进程所能打开的最大连接数有 FD_SETSIZE 宏定义,其大小是 32 个整数的大小(在32 位的机器上,大小就是32*32,同理 64 位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响。
poll:
poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
epoll:
连接数基本上只受限于机器的内存大小。
2、FD剧增后带来的IO效率问题
select:
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
poll:同上。
epoll:
因为 epoll 内核中实现是根据每个 fd 上的 callback函数来实现的,只有活跃的 socket 才会主动调用 callback,所以在活跃 socket 较少的情况下,使用 epoll 没有前面两者的线性下降的性能问题,但是所有 socket 都很活跃的情况下,可能会有性能问题。
3、消息传递方式
select:
内核需要将消息传递到用户空间,都需要内核拷贝动作。
poll:同上。
epoll:
epoll通过内核和用户空间共享一块内存来实现的。
总结
在选择 select,poll,epoll 时要根据具体的使用场合以及这三种方式的自身特点。
1、表面上看 epoll 的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和 poll 的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多函数回调。
2、select 低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。