apue高级IO

非阻塞 I/O

之前讨论的所有函数都是阻塞的,如read(2)函数读取设备时,设备中如果没有充足的数据,那么read(2)函数就会阻塞等待,直到有数据可读再返回。当 IO 操作时出错时,需要判断是真的出错还是假错。

两种假错的情况:

  • EINTR:被信号打断,阻塞时会遇到。
  • EAGAIN:非阻塞形式操作失败。

遇到这两种假错的时候我们需要重新再操作一次(占用CPU),所以通常对假错的判断是放在循环中的。例如read(2)函数使用非阻塞方式读取数据时,如果没有读取到数据,errnoEAGAIN,表明是非阻塞方式读取返回了,并非设备有问题或读取失败。

阻塞与非阻塞是使用的同一套函数,flags特殊要求指定为O_NONBLOCK就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fd = open("/etc/service", O_RDONLY | O_NONBLOCK); //以非阻塞方式打开
/* if error */

while (1) {
size = read(fd, buf, BUFSIZE);
if (size < 0) {
if (EAGAIN == errno) {
continue; //发生假错,重复read
}
perror("read()");
exit(1);
}
}
// do sth...

IO多路转接

IO密集型的任务,可以使用IO多路转接。select(),poll(),epoll()

先布置监视任务(设置自己关心/感兴趣的事件/文件描述符),监视多个IO(文件描述符),当某个文件描述符发生了我们感兴趣的事情(读/写)的时候再去操作(读/写)。

select(2)移植性高,各个平台都支持它,这也是它相对于poll(2)唯一的优点。

select(2)关心的是事件,poll()关心的是文件描述符。

select(2)poll()都是可移植的,epoll()是Linux平台实现的方言。epoll()是利用poll()封装的。

select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//IO多路转接(IO复用)
//fd_set 文件描述符集合
//nfds:你所监视fd_set中最大的那个再加1(如:监视1,4,那nfds取5,因为内核用的是<nfds,不加1就无法监视最大那个)
//readfds:你关心哪些文件描述符的可读事件
//writefds:你关心哪些文件描述符的可写事件
//exceptfds:你关心哪些文件描述符的异常事件
//timeout:阻塞的超时时间,如果不设置则阻塞等到关心事件发生才返回
//返回:
// 表示有多少个文件描述符发生了你关心的事件,发生的事件回填到了readfds,writefds,exceptfds里面(下次又得重新设置这3个再调用select)
// 出错返回-1,并设置errno(若设置了超时,超时过后返回-1,errno设为EINTR(假错))
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set); //从set中删除fd
int FD_ISSET(int fd, fd_set *set); //判断fd是否在set中
void FD_SET(int fd, fd_set *set); //把fd加入set中
void FD_ZERO(fd_set *set); //把set清空

select(2)可相当于一个安全可靠的休眠。某些平台的sleep()用的是信号,不是很安全。

查看代码

1
2
./io/adv/select/relay.c
#define use_select (1)

poll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//等待某个事件在指定的文件描述符上发生
//fds:结构体数组
//nfds:结构体数组长度(有多少个文件描述符)
//timeout:超时设置(毫秒),0-非阻塞,-1-阻塞
//返回:
// 正数,有多少个事件发生了
// 0,没有发生事件,超时返回
// -1,出错并设置errno(EINTR是假错)
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
int fd; //file descriptor 文件描述符
short events; // requested events 关心的事件
short revents; // returned events 发生的事件
};

关心的事件和发生的事件是分开的两个成员,所以关心的事件发生一次后不用重新设置。

epoll

1
2
3
4
5
6
7
//创建一个epoll文件描述符(创建数组)
//size:忽略(随便填个正数)
//返回:
// 正数,成功,表示有多少个文件描述符。
// -1,出错并设置errno
int epoll_create(int size);
int epoll_create1(int flags);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//控制epoll文件描述符
//对epfd实例用的fd进行op操作event
//epfd:用epoll_create创建的实例
//op:EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL
//fd:目标文件描述符
//event:目标的事件
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);

typedef union epoll_data {
void *ptr;
int fd; //目标文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event {
uint32_t events; // Epoll events 关心的事件(位图)
epoll_data_t data; // User data variable
};
1
2
3
4
5
6
7
8
9
10
11
12
13
//往外取正在发生的事件
//epfd:用epoll_create创建的实例
//event:取出来的事件回填的地址(数组)
//maxevents:想要取多少个事件
//timeout:超时设置(毫秒),0-非阻塞,-1-阻塞
//返回:正数,有多少个文件描述符发生了关心的事件
// 0,没有发生事件超时返回
// -1,出错并设置errno(EINTR是假错)
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);

poll是自己定义一个数组,每个元素就是一个文件描述符以及其关心和发生的事件,epoll是内核帮你维护了这样一个数组。epoll_create()就是创建这个数组,再用其他函数去访问/修改该数组。

select&poll&epoll

select()缺点:

1.fd_set使用数组实现,fd_size有限制 1024bitmapfd[i] = accept()
2.fdset不可重用,新的fd进来,重新创建
3.用户态和内核态拷贝bitmap产生开销
4.O(n)时间复杂度的轮询(发生事件后的轮询)

poll基于结构体存储fd,解决了select的缺点1,2

1
2
3
4
5
struct pollfd{
int fd;
short events;
short revents; //可重用
}

epoll:用户态和内核态不拷贝bitmap;返回结果不需要轮询,时间复杂度为O(1),解决select的缺点1,2,3,4

epoll_create 创建一个白板 存放fd_events
epoll_ctl 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上
epoll_wait 通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符

两种触发模式:
LT:水平触发
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
ET:边缘触发
和 LT 模式不同的是,通知之后进程必须立即处理事件。
下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数,
因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。