Linux 中,read 和 write 函数默认实现的是阻塞式的 IO。例如:
while ((n = read(STDIN_FILENO, buf, BUFSIZ) > 0) {
if (write(STDOUT_FILENO, buf, n) != n) {
perror("write");
eixt(EXIT_FAILURE);
}
}
如果需要同时从多个描述符读,则不能阻塞。
异步 IO(asynchronous IO)借助内核监控描述符的状态,当描述符可以进行 IO 时,内核会向进程发送一个信号。但是这种方式有两个缺点:
- 兼容性差,各个 UNIX 具体版本接口不一
- 可用信号只有一个(SIGPOLL 或 SIGIO),最多只能用于一个描述符
IO 多路转接(IO multiplexing)使用时,需要先构造一张描述符列表,然后调用多路转接函数并阻塞。当至少一个描述符准备好 IO 时,函数返回。
select 和 poll 对比
select 函数的缺点
- 最大监听描述符受限,最大1024(进程默认最大打开1024个文件,即使修改了也不影响 select)
- 入参和返回参数同一个,每次调用都需要重新初始化入参
- 内核和应用程序每次都需要顺序扫描描述符,直到入参指定的最大描述符ID,低效
poll 函数比 select 有所改进
- 通过
vim /etc/security/limits.conf
修改进程最大可打开的描述符限制后,可以扩大 poll 监听的描述符数量(可以先用cat /proc/sys/fs/file-max
查看硬件限制) - 入参跟返回参数独立,不需要每次调用前都初始化
select 和 pselect 函数
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_ZERO(fd_set *set); /* 清空位图 */
void FD_SET(int fd, fd_set *set); /* 指定描述符在位图中置位 */
void FD_CLR(int fd, fd_set *set); /* 清空位图指定描述符对应的位 */
int FD_ISSET(int fd, fd_set *set); /* 判断指定描述符是否在位图中置位 */
fd_set 实际是个位图,每个位表示一个文件描述符。下面4个函数专门用来操作这个位图。
传入传出参数:函数会直接修改传入的参数。
参数:
- nfds:最大的描述符序号加一。0/1/2 被标准输入输出和错误流占用,用户可用描述符序号从 3 开始。用于指示内核扫描列表的数据量。
- readfds:传入传出参数。监听哪些描述符的可读事件,并返回哪些已经准备好了。
- writefds:传入传出参数。监听哪些描述符的可写事件
- exceptfds:传入传出参数。监听哪些描述符的异常事件
- timeout:超时时间,两种格式:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds 微秒 */
};
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds 毫秒 */
};
返回值:总的满足条件的描述符个数,不同事件下的同一个描述符可以累加。例如监听 fd1 的读写事件,这两个事件同时满足时返回值会加2。
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
select 代码示例
下面创建10个管道,用select函数同时监控所有管道的读端,并定时向写端写数据。设置了10s的超时时间,超时后select函数会返回0:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <pthread.h>
#include <string.h>
int pipefds[10][2];
void *my_fun(void *args) {
int i;
char buf[10] = "hello wor";
for (i = 0; i < 10; i++) {
write(pipefds[i][1], buf, strlen(buf));
sleep(1);
}
return (void*)0;
}
int main() {
fd_set readfds;
int i, ret;
pthread_t tid;
struct timeval tv;
char buf[BUFSIZ];
FD_ZERO(&readfds); // 清空 读描述符集合
tv.tv_sec = 10;
tv.tv_usec = 0;
if (pthread_create(&tid, NULL, my_fun, NULL) != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}
while(1) {
for (i = 0; i < 10; i++) {
if (pipe(pipefds[i]) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
/* 因为select函数返回时会修改描述符集合,所以每次都需要初始化 */
FD_SET(pipefds[i][0], &readfds);
}
ret = select(pipefds[9][1] + 1, &readfds, NULL, NULL, &tv);
if (ret == 0) {
printf("time out\n");
return 0;
}
printf("ret of select is %d\n", ret);
for (i = 0; i < 10; i++) {
if (FD_ISSET(pipefds[i][0], &readfds)) {
read(pipefds[i][0], buf, sizeof(buf));
printf("seq is:%d, data is:%s\n", i, buf);
}
}
}
return 0;
}
poll 代码示例
下面创建10个管道,用 poll 函数同时监控所有管道的读端,并定时向写端写数据。设置了负数超时时间,所以进程会永远等在这里:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <pthread.h>
int pipefds[10][2];
void * func(void *args) {
int i;
char buf[20] = "hello world\n";
for (i = 0; i < 10; i++) {
write(pipefds[i][1], buf, strlen(buf));
sleep(1);
}
return (void *)0;
}
int main() {
struct pollfd pfds[10];
int ret, i;
char buf[BUFSIZ];
pthread_t tid;
for (i = 0; i < 10; i++) {
ret = pipe(pipefds[i]);
if (ret < 0) {
perror("pipe");
return -1;
}
}
ret = pthread_create(&tid, NULL, func, NULL);
if (ret < 0) {
perror("pthread_create");
return -1;
}
for (i = 0; i < 10; i++) {
pfds[i].fd = pipefds[i][0];
pfds[i].events = POLLIN;
}
for ( ; ; ) {
ret = poll(pfds, 10, -1);
if (ret < 0) {
perror("poll");
return -1;
}
for (i = 0; i < 10; i++) {
if (pfds[i].revents & POLLIN) {
read(pipefds[i][0], buf, sizeof(buf));
puts(buf);
}
}
}
return 0;
}