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;
}