内容
- 基本概念
- 了解接口socket
- 了解协议tcp/udp
- io复用
- select
- poll/epoll
可以参考学习的文章:
- 深入浅出理解select、poll、epoll的实现
- linux在系统调用进入内核时,为什么要将参数从用户空间拷贝到内核空间?不能直接访问,或是使用memcpy吗?非要使用copy_from_user才行吗? - 针对为什么select/poll调用一次拷贝一次数组到内核态空间。
I/O复用
- select
- poll
- epoll
select
先观察其API
1 |
|
- 参数
nfds参数通常被设置为select监听的所有文件描述符中的最大值加1,表示在fd_set集合中,我们关心的描述符的总数。为什么加1呢?因为文件描述符是从0开始计数的。nfds与fd_set容量大小不一样,容量大小指的是FD_SETSIZE,即fd_set容量大小是fd_set可容纳描述符的最大大小。- readfds参数是select关心的读事件的集合;
- writefds参数是select关心的写事件的集合;
- exceptfds参数select关心的异常事件的集合;
- timeout参数设置select的超时时间。
- 返回值
- 集合中有事件就绪的描述符的个数
- 但是并没有告诉你具体是哪一个描述符就绪
fd_set结构体
1 |
|
其中,__FD_SETSIZE指出select可以关注的最大文件描述符个数,默认为1024。
__fd_mask被定义为long int的类型别称,long int在32位机占8个字节。
__NFDBITS计算的是1个__fd_mask元素所占用的位数,一个字节占8位,sizeof算出__fd_mask的字节数,相乘得其占用的bit大小。
接着,定义fds_bits,其是long int型的数组,数组大小为__FD_SETSIZE除以__NFDBITS。比如SETSIZE为1024位,NFDBITS是64位,则数组大小位1024/64=16。这里的计算主要是为了计算出数组的大小,以确定多大的数组可以正好容纳1024个位数,来记录文件描述符信息。
用到的宏函数
fd_set集合对于文件描述符的管理是按位进行的,而位只有0和1两种状态。
假如SETSIZE=1024,则可管理1024个文件描述符,如果文件描述符7有效,我们需要对位操作,使其位变为1。
由于位操作过于繁琐,select API中提供了一系列宏函数来方便我们访问、操作fd_set集合状态。
1 |
|
select编程思路
最好另外定义一个整型数组,其大小为我们预测将要出现的描述符的最多数目。用作我们存放描述符的容器。初始化时将数组值一律设为-1,表示容器中该位置还没有存放描述符。如果在某一时刻有一个描述符有了消息,我们就将该描述符数值覆盖到这个容器中第一个为-1的地方。
1 |
|
示例-TCP服务使用select处理多个套接字
1 | int main() |
场景情况:如果客户端与select服务端已建立连接,而客户端进程结束,select会一直阻塞、未感知吗?–不会。
因为客户端的进程结束,也算是一种读事件,相当于通知服务端该套接字连接结束了。那么服务端recv会返回0,达到关闭该套接字的条件,关闭后,别忘了在fds容器中删除掉该描述符。
如果忘记了close该套接字,且忘了fds_del该描述符,那么如果客户端结束进程,服务端就会一直打印"client close",因为select一直在探测此描述符有无读事件,若该套接字连接关闭,那么此描述符一直有读事件,recv返回0,由于没有fds_del,每次都会关注,所以每次都会打印"client close"。
poll
可以理解为加强版的select。
先观察其API
1 |
|
-
fds参数是一个
pollfd结构类型的指针,可指向一段连续空间(数组),因此很灵活,大小可按需声明。它可以指定我们感兴趣的文件描述符上发生的可读、可写和异常等事件。定义如下1
2
3
4
5
6struct pollfd
{
int fd; //文件描述符
short events; //注册的事件类型,按位标志
short revents; //实际发生的事件,按位标志,由内核填充
}- 其中,fd成员指定文件描述符。
- events成员告诉poll监听fd上的哪些事件类型,他可以是一系列事件类型的按位或。常见的事件类型有:
POLLIN(数据可读)、POLLOUT(数据可写)。 - revents成员由内核修改,以通知应用程序fd上实际发生了哪些事件。
poll编程
1 | void poll_fds_init(struct pollfd* fds) |
与select的一处细节区别:
每次select除了监测fd_set有效描述符上有无事件,其次还将没有事件的描述符从fd_set移除(将该描述符对应在fd_set上的位进行置0操作)(这样就得每次select之前都要重新注册一遍我们关注的描述符(即用户和内核共同操作FD_SET、FD_CLR等)),然后下面过滤有事件的描述符时,只要找到fd_set集合哪个位是1状态即就找到了有事件产生的描述符。
而poll的用法是:用户只管注册events,实际上的有无事件由内核来进行对revents的填充,以此来更好地区别该描述符是否有事件产生。这样,就不用在每次poll之前重新注册一遍我们关注的描述符的结构体里的events了。我们只要把要关心的描述符的fd置成非-1,以及管理好要关心的哪些事件类型events即可。
与select相比的优点
- 可以监听的描述符的最大数目可以超过
1024个,大小按需自拟。 - 可以监听的事件类型数目变多、变细了,更强大了。
- 不用在每次
poll之前重新注册一遍我们关注的描述符的结构体里的events了
总结
select和poll的总体实现流程:
- 用户程序
select用fd_set结构体(默认是1024个bit位的数组);poll用struct pollfd fds[MAX]结构体。
select和poll通常被循环调用。每调用一次,就拷贝一次结构体数组给内核。
linux在系统调用进入内核时,为什么要将参数从用户空间拷贝到内核空间?不能直接访问,或是使用memcpy吗?非要使用copy_from_user才行吗? - 针对为什么select/poll调用一次拷贝一次数组到内核态空间。
- 内核轮询
内核对若干个文件描述符进行轮询扫描。查看之上是否有事件发生。
时间复杂度,为O(n) - select/poll返回后
返回值仅仅告诉用户程序发生了事件的描述符个数,并未告诉具体哪个描述符。
用户程序还需要再次轮询一遍。O(n)

