塞和非阻塞
从简单的开始,我们以经典的读取文件的模型举例。(对操作系统而言,所有的输入输出设备都被抽象成文件。)
在发起读取文件的请求时,应用层会调用系统内核的I/O接口。
如果应用层调用的是阻塞型I/O,那么在调用之后,应用层即刻被挂起,一直出于等待数据返回的状态,直到系统内核从磁盘读取完数据并返回给应用层,应用层才用获得的数据进行接下来的其他操作。
如果应用层调用的是非阻塞I/O,那么调用后,系统内核会立即返回(虽然还没有文件内容的数据),应用层并不会被挂起,它可以做其他任意它想做的操作。(至于文件内容数据如何返回给应用层,这已经超出了阻塞和非阻塞的辨别范畴。)
这便是(脱离同步和异步来说之后)阻塞和非阻塞的区别。总结来说,是否是阻塞还是非阻塞,关注的是接口调用(发出请求)后等待数据返回时的状态。被挂起无法执行其他操作的则是阻塞型的,可以被立即「抽离」去完成其他「任务」的则是非阻塞型的。
同步和异步
阻塞和非阻塞解决了应用层等待数据返回时的状态问题,那系统内核获取到的数据到底如何返回给应用层呢?这里不同类型的操作便体现的是同步和异步的区别。
对于同步型的调用,应用层需要自己去向系统内核问询,如果数据还未读取完毕,那此时读取文件的任务还未完成,应用层根据其阻塞和非阻塞的划分,或挂起或去做其他事情(所以同步和异步并不决定其等待数据返回时的状态);如果数据已经读取完毕,那此时系统内核将数据返回给应用层,应用层即可以用取得的数据做其他相关的事情。
而对于异步型的调用,应用层无需主动向系统内核问询,在系统内核读取完文件数据之后,会主动通知应用层数据已经读取完毕,此时应用层即可以接收系统内核返回过来的数据,再做其他事情。
这便是(脱离阻塞和非阻塞来说之后)同步和异步的区别。也就是说,是否是同步还是异步,关注的是任务完成时消息通知的方式。由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式是异步调用。
举例说明:
老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。 1 老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻 2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。 3 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大 4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言。 普通水壶,同步;响水壶,异步。 虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。 同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。 立等的老张,阻塞;看电视的老张,非阻塞。 情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
除了 POSIX 异步 IO 以外,还有 System V 异步 IO、BSD 异步 IO。这里只讲 POSIX 异步IO,对应aio开头的一系列函数:
函数 | 语义 |
---|---|
aio_read | 请求异步读操作 |
aio_write | 请求异步写操作 |
aio_error | 检查异步请求的状态 |
aio_return | 获得完成的异步请求的返回状态 |
aio_suspend | 阻塞调用进程,直到一个或多个异步请求已经完成(或失败) |
aio_cancel | 取消异步 I/O 请求 |
lio_listio | 发起一系列 I/O 操作 |
异步 IO 控制块(asynchronous I/O control block, aiocb)
#include
struct aiocb {
/* 下面所有字段依赖于具体实现 */
int aio_fildes; /* 文件描述符 */
off_t aio_offset; /* 文件偏移 */
volatile void* aio_buf; /* 缓冲区地址 */
size_t aio_nbytes; /* 传输的数据长度 */
int aio_reqprio; /* 请求优先级 */
struct sigevent aio_sigevent; /* 通知方法 */
int aio_lio_opcode; /* 仅被 lio_listio() 函数使用 */
/* Various implementation-internal fields not shown */
};
aio_fildes: 文件描述符,相当于read 或 write 函数的第一个fd参数,表示想操作哪个文件描述符上的IO。
aio_offset: 文件偏移指针,表示想从文件的哪个位置开始操作。比如从文件的第10个字节开始读,就设成10。
aio_buf: 缓冲区的地址。
aio_nbytes: 要传输多少字节的数据。
实验:异步读操作的例子
// my_aio_read.c
#include
#include
#include
#include
#include
#include
// 需要包含 aio.h 文件
#include
#include
#include
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
int main() {
int fd, ret;
char buf[64];
// 定义一个异步控制块结构体,不懂没关系,不用管
struct aiocb my_aiocb;
// 初始化
bzero((char*)&my_aiocb, sizeof(struct aiocb));
my_aiocb.aio_buf = buf; // 告诉内核,有数据了就放这儿
my_aiocb.aio_fildes = STDIN_FILENO; // 告诉内核,想从标准输入读数据
my_aiocb.aio_nbytes = 64; // 告诉内核,缓冲区大小只有 64
my_aiocb.aio_offset = 0; // 告诉内核,从偏移为 0 的地方开始读
// 发起异步读操作,立即返回。你并不知道何时 buf 中会有数据
ret = aio_read(&my_aiocb);
if (ret < 0) ERR_EXIT("aio_read");
// 不断的检查异步读的状态,如果返回 EINPROGRESS,说明异步读还没完成
// 轮询检查状态是一种很笨的方式,其实可以让操作系统用信号的方式来通知,或者让操作系统完成读后主动创建一个线程执行。
while (aio_error(&my_aiocb) == EINPROGRESS) {
write(STDOUT_FILENO, ".", 1);
sleep(1);
}
// 打印缓冲区内容,你并不知道内核是什么时候将缓冲区中的 hello 复制到你的 buf 中的。
printf("content: %s\n", buf);
return 0;
}
注意编译的时候需要链接 rt 运行库。
$ gcc my_aio_read.c -o my_aio_read -lrt1
运行后,如果你什么也不操作,程序会在屏幕上打点。这里,我输入了 hello 后回车。
$ ./my_aio_read
...he.llo..
content: hello
使用 aio_read 或 aio_write 等函数发起了异步读或写时,内核就自己去干活了,轮询查看结果。还可以异步通知,后面讲。
// 不断的检查异步读的状态,如果返回 EINPROGRESS,说明异步读还没完成
// 轮询检查状态是一种很笨的方式,其实可以让操作系统用信号的方式来通知,或者让操作系统完成读后主动创建一个线程执行。在后面我们会继续学习这两种通知方式。
while (aio_error(&my_aiocb) == EINPROGRESS) {
write(STDOUT_FILENO, ".", 1);
sleep(1);
}
作用:获取异步请求的状态。
int aio_error(const struct aiocb *aiocb);
返回值:
EINPROGRESS,异步请求未完成。
ECANCELED,异步请求被取消。
0,请求成功完成。
> 0 的错误码,表明异步操作失败,该值相当于同步IO函数 read、write 出错时,设置的errno变量。
aio_error 是线程安全的。
作用:获取异步请求的状态。
ssize_t aio_return(struct aiocb *aiocbp);
注意:对于每个请求只能使用一次,而且要在 aio_error 返回值不是 EINPROGRESS 的情况下使用。
返回值:
如果异步操作完成了,该函数返回值就相当于同步IO类函数 read, write, fsync 或 fdatasync 等的返回值。
如果异步操作未完成的情况下你使用了它,结果是未定义的!
实验:程序 my_aio_return.c 实际上只是将前面的代码稍稍做了修改。
// my_aio_return.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
int main() {
int fd, ret;
char buf[64] = { 0 };
struct aiocb my_aiocb;
bzero((char*)&my_aiocb, sizeof(struct aiocb));
my_aiocb.aio_buf = buf;
my_aiocb.aio_fildes = STDIN_FILENO;
my_aiocb.aio_nbytes = 64;
my_aiocb.aio_offset = 0;
ret = aio_read(&my_aiocb);
if (ret < 0) ERR_EXIT("aio_read");
while (aio_error(&my_aiocb) == EINPROGRESS) {
sleep(1);
}
// 获取最终的异步操作状态
ret = aio_return(&my_aiocb);
if (ret < 0) ERR_EXIT("aio_return");
// 打印内容和返回值。
printf("content: %s, return: %d\n", buf, ret);
return 0;
}
编译运行
$ gcc my_aio_return.c -o my_aio_return -lrt
$ ./my_aio_return
hello
content: hello
, return: 6
程序启动后,在终端输入了字符串 hello
后回车。然后在程序的界面打印出 hello 的内容,注意后面换行符也被送到缓冲区了,aio_return 返回的是读到的字节数。
类似线程中的 pthread_join 函数,在异步 IO 中,aio_suspend等待指定的异步 IO 操作完成才返回。
int aio_suspend(const struct aiocb * const aiocb_list[], int nitems, const struct timespec *timeout);
参数
aiocb_list:数组,存储的元素类型是 const struct aiocb* 类型。如果某个元素为 NULL,aio_suspend 会忽略它。
nitems:aiocb_list 数组大小。
timeout:超时时间,设置成 NULL 表示永远等待,直到异步 IO 操作完成。
函数语义
aio_suspend 函数会阻塞调用线程,直到发生下面的事情:
实验:
程序 my_aio_suspend 同样是对前面程序的修改。
// my_aio_suspend.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
int main() {
int fd, ret;
char buf[64] = { 0 };
struct aiocb my_aiocb;
bzero((char*)&my_aiocb, sizeof(struct aiocb));
my_aiocb.aio_buf = buf;
my_aiocb.aio_fildes = STDIN_FILENO;
my_aiocb.aio_nbytes = 64;
my_aiocb.aio_offset = 0;
ret = aio_read(&my_aiocb);
if (ret < 0) ERR_EXIT("aio_read");
// 为了传递给 aio_suspend 用,创建一个大小为 5 的数组
const struct aiocb* aio_list[5] = { NULL };
// 将其一中元素赋值,不一定是第 0 个,随便啊。
aio_list[0] = &my_aiocb;
// 只要 my_aiocb 这个异步读还没完成,aio_suspend 就会阻塞
ret = aio_suspend(aio_list, 5, NULL);
if (ret < 0) ERR_EXIT("aio_suspend");
puts("aio_suspend return");
// 实际上,这个 while 循环我们不可能看到它执行的。写在这里只是为了演示。
while (aio_error(&my_aiocb) == EINPROGRESS) {
puts("EINPROGRESS");
}
// 获取返回值。
ret = aio_return(&my_aiocb);
if (ret < 0) ERR_EXIT("aio_return");
printf("content: %s, return: %d\n", buf, ret);
return 0;
}
编译运行
$ gcc my_aio_suspend.c -o my_aio_suspend -lrt
$ ./my_aio_suspend
hello
aio_suspend return
content: hello
, return: 6
启动程序后,程序首先会在 aio_suspend 处阻塞,在终端输入 hello 后,aio_suspend 就返回了。
int aio_cancel(int fd, struct aiocb *aiocbp)
参数
fd:想取消哪个描述符上的异步 IO 请求。
aiocbp:空,表示取消该描述符上所有的异步 IO 请求,不空,取消由 aiocbp 指定的异步 IO 请求。
返回值
AIO_CANCELED:所有请求被成功取消。
AIO_NOTCANCELED:至少有一个请求未被取消,因为它处于异步 IO 的处理过程中,也就是 aio_error 返回值为 EINPROGRESS 的时候。
AIO_ALLDONE:所有请求都已经完成了。
-1:有错误发生,同时设置 errno 变量。
实验:
程序 my_aio_cancel.c 演示了 aio_cancel 的使用。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
int main() {
int fd, ret;
char buf[64] = { 0 };
struct aiocb my_aiocb;
bzero((char*)&my_aiocb, sizeof(struct aiocb));
my_aiocb.aio_buf = buf;
my_aiocb.aio_fildes = STDIN_FILENO;
my_aiocb.aio_nbytes = 64;
my_aiocb.aio_offset = 0;
// 发起异步读请求
ret = aio_read(&my_aiocb);
if (ret < 0) ERR_EXIT("aio_read");
// 取消异步读请求
ret = aio_cancel(STDIN_FILENO, &my_aiocb);
if (ret == AIO_CANCELED)
puts("AIO_CANCELED");
else if (ret == AIO_NOTCANCELED)
puts("AIO_NOTCANCELED");
else if (ret == AIO_ALLDONE)
puts("AIO_ALLDONE");
else if (ret == -1)
ERR_EXIT("aio_cancel");
while((ret = aio_error(&my_aiocb)) == EINPROGRESS) {
sleep(1);
}
if (ret == ECANCELED)
puts("ECANCELED");
else if (ret == 0)
puts("Request completed");
else if (ret > 0) {
errno = ret;
ERR_EXIT("aio_error");
}
ret = aio_return(&my_aiocb);
if (ret < 0) ERR_EXIT("aio_return");
printf("content: %s, return: %d\n", buf, ret);
return 0;
}
编译运行
$ gcc my_aio_cancel.c -o my_aio_cancel -lrt
$ ./my_aio_cancel
AIO_NOTCANCELED
hello
Request completed
content: hello
, return: 6
从结果里可以看到,无法取消前面发起的异步读操作。
POSIX 提供了函数 lio_listio 可以一次性发起多个异步 IO 请求。
int lio_listio(int mode, struct aiocb *const aiocb_list[], int nitems, struct sigevent *sevp);
函数参数
值 | 含义 |
---|---|
LIO_WAIT | lio_listio阻塞,直到所有的异步IO请求完成。此时参数 sevp 被忽略掉 |
LIO_NOWAIT | lio_listio立即返回,当所有异步IO请求完成后,进行异步通知,通知方式由参数 sevp 指定,为 NULL表示不需要异步通知。 |
aiocb_list:数组数组元素为aiocb 结构体。
nitems:数组大小。
在使用 lio_listio 函数时,需要将 aiocb 中的 aio_lio_opcode 成员赋值,说明内核发起何种异步IO操作。可以取如下值:
值 | 含义 |
---|---|
LIO_READ | 发起异步读操作 |
LIO_WRITE | 发起异步写操作 |
LIO_NOP | 忽略掉aiocb |
不管sevp参数,设为NULL。
实验:
程序 my_aio_lio 修改了前面的代码,将 aio_read 函数替换成了 lio_listio 函数发起异步读请求。程序中并没有演示同时发起多个请求,只是发起了一个。实际上,一个你会了,多个也没什么问题,无非就是给数组赋几个值而已
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
int main() {
int fd, ret;
char buf[64];
struct aiocb my_aiocb;
bzero((char*)&my_aiocb, sizeof(struct aiocb));
my_aiocb.aio_buf = buf;
my_aiocb.aio_fildes = STDIN_FILENO;
my_aiocb.aio_nbytes = 64;
my_aiocb.aio_offset = 0;
// 注意这里多了一个成员的赋值,aio_lio_opcode 成员是专门给 lio_listio 函数用的。
my_aiocb.aio_lio_opcode = LIO_READ;
// 定义一个大小为 5 的数组,多大无所谓了,看你需求了。
struct aiocb* aio_list[5] = { NULL };
// 将你想发起的请求的控制块放到数组里,放哪个位置都行。这里我只发起了一个读操作。
aio_list[3] = &my_aiocb;
// 调用 lio_listio 发起读请求
ret = lio_listio(LIO_NOWAIT, aio_list, 5, NULL);
while (aio_error(&my_aiocb) == EINPROGRESS) {
write(STDOUT_FILENO, ".", 1);
sleep(1);
}
printf("content: %s\n", buf);
return 0;
}
编译运行
$ gcc my_aio_lio.c -o my_aio_lio -lrt
$ ./my_aio_lio
....hell.o
content: hello
1、 使用 while 循环和 aio_error 函数轮询。
while(aio_error(&my_aiocb) == EINPROGRESS) {
write(STDOUT_FILENO, ".", 1);
sleep(1);
}
2、采用异步的方式通知程序异步操作已完成。
struct aiocb {
// ...
struct sigevent aio_sigevent; /* 通知方法 */
// ...
};
aio_sigevent是一个结构体,组成如下。
union sigval { /* Data passed with notification */
int sival_int; /* Integer value */
void* sival_ptr; /* Pointer value */
};
struct sigevent {
int sigev_notify; /* 通知方式 */
int sigev_signo; /* 通知所用的信号,可以自己指定,比如 SIGUSR1 */
union sigval sigev_value; /* 通知附带的数据 */
// 下面两个成员仅仅用于 SIGEV_THREAD 通知方式
void (*sigev_notify_function) (union sigval); /* 线程通知函数 */
void* sigev_notify_attributes; /* 通知线程的属性,一般指定为 pthread_attr_t 结构的地址 */
// 通知线程 id. 这个成员仅仅用于 SIGEV_THREAD_ID 通知方式,这种通知方式我们不学它。
pid_t sigev_notify_thread_id;
};
(1) sigev_notify
异步 IO 完成后,用什么样的方式通知用户程序。四个可选值:
SIGEV_NONE:表示不通知。
SIGEV_SIGNAL:异步 IO 操作完成后,程序收到指定的信号(成员 sigev_signo)。
SIGEV_THREAD:当异步 IO 操作完成后,内核会创建一个新线程执行一个函数,函数由成员 sigev_notify_function 指定。函数参数是sigev_value。
SIGEV_THREAD_ID:Linux 操作系统独有的。仅用于 POSIX 定时器中。不管。
(2) sigev_signo
如果使用SIGEV_SIGNAL方式通知,异步IO操作完成后,进程会收到这个成员指定的信号。
(3) sigev_value
产生通知时,所附加的数据。通知产生时,通过带参数的信号处理函数或线程函数的参数传递过来。
(4) sigev_notify_function
线程函数,仅仅用于 SIGEV_THREAD 通知方式。
(5) sigev_notify_attributes
创建线程的属性,pthread_attr_t 类型的指针。仅用于 SIGEV_THREAD 通知方式。
(6) sigev_notify_thread_id
仅仅用于 SIGEV_THREAD_ID 通知方式,不学。
实验:
这部分有两个实验,一个用信号的方式通知,另一个是用线程的方式。
// my_aio_sig.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
#define IO_SIGNAL SIGUSR1
// 信号处理函数
void handler(int sig, siginfo_t* info, void* ucontext) {
int ret;
printf("receive signal: %d\n", sig);
struct aiocb* my_aiocb = info->si_value.sival_ptr;
while (aio_error(my_aiocb) == EINPROGRESS) {
write(STDOUT_FILENO, ".", 1);
}
ret = aio_return(my_aiocb);
if (ret < 0) ERR_EXIT("aio_return");
printf("content: %s\n", (char*)(my_aiocb->aio_buf));
exit(0);
}
int main() {
int fd, ret;
struct aiocb my_aiocb;
char buf[64];
bzero((char*)&my_aiocb, sizeof(struct aiocb));
// 注册信号处理函数
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = handler;
sigaction(IO_SIGNAL, &sa, NULL);
my_aiocb.aio_buf = buf;
my_aiocb.aio_fildes = STDIN_FILENO;
my_aiocb.aio_nbytes = 64;
my_aiocb.aio_offset = 0;
// 设置通知方式为信号通知
my_aiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
// 设置通知信号
my_aiocb.aio_sigevent.sigev_signo = IO_SIGNAL;
// 通知附加数据。这个成员将来会传递到信号处理函数中。
my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;
ret = aio_read(&my_aiocb);
if (ret < 0) ERR_EXIT("aio_read");
while (1) {
pause();
}
return 0;
}
编译运行
$ gcc my_aio_sig.c -o my_aio_sig -lrt
$ ./my_aio_sig
hello
receive signal: 10
content: hello
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
// 线程函数
void handler(union sigval val) {
int ret;
struct aiocb* my_aiocb = val.sival_ptr;
while (aio_error(my_aiocb) == EINPROGRESS) {
write(STDOUT_FILENO, ".", 1);
}
ret = aio_return(my_aiocb);
if (ret < 0) ERR_EXIT("aio_return");
printf("content: %s\n", (char*)(my_aiocb->aio_buf));
exit(0);
}
int main() {
int fd, ret;
struct aiocb my_aiocb;
const struct aiocb* aio_list[1] = { &my_aiocb };
char buf[64];
bzero((char*)&my_aiocb, sizeof(struct aiocb));
my_aiocb.aio_buf = buf;
my_aiocb.aio_fildes = STDIN_FILENO;
my_aiocb.aio_nbytes = 64;
my_aiocb.aio_offset = 0;
// 通知方式设置为线程通知
my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
// 通知附加数据
my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;
// 线程函数
my_aiocb.aio_sigevent.sigev_notify_function = handler;
ret = aio_read(&my_aiocb);
if (ret < 0) ERR_EXIT("aio_read");
ret = aio_suspend(aio_list, 1, NULL);
if (ret < 0) ERR_EXIT("aio_suspend");
while (1) pause();
puts("main exited");
return 0;
}
编译运行
$ gcc my_aio_thread.c -o my_aio_thread -lrt
$ ./my_aio_thread
hello
content: hello