五种IO模型
- 阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.
- 非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
- 信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
- IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.
- 异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
任何IO过程中, 都包含两个步骤:
第⼀是等待, 第二是拷贝.
而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.
同步通信&异步通信
- 同步和异步关注的是消息通信机制.
同步:就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回, 就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
- 多进程多线程的时候, 也提到同步和互斥
进程/线程同步也是进程/线程之间直接的制约关系
是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。尤其是在访问临界资源的时候
阻塞&非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回.
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.
非阻塞IO
一个文件描述符,默认都是阻塞IO
将阻塞IO设置成非阻塞IO的函数时 fcntl
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl函数有5种功能:
复制一个现有的描述符(cmd=F_DUPFD).
获得/设置文件描述符标记(cmd=FGETFD 或 FSETFD)
获得/设置文件状态标记(cmd=FGETFL 或 FSETFL)
获得/设置异步I/O所有权(cmd=FGETOWN 或 FSETOWN)
获得/设置记录锁(cmd=FGETLK,FSETLK或F_SETLKW
用第三种功能, 获取/设置文件状态标记, 就可以将一个⽂文件描述符设置为非阻塞.
1 /*
2 * 通过这个函数把文件描述符设置为非阻塞
3 */
4
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <unistd.h>
8 #include <fcntl.h>
9
10 int SetNoBlack(int fd)
11 {
12 int flag=fcntl(fd,F_GETFL);
13 if(flag<0){
14 perror("fcntl error");
15 return -1;
16 }
17 int ret=fcntl(fd,F_SETFL,flag | O_NONBLOCK);
18 if(ret<0){
19 perror("fcntl error");
20 return -1;
21 }
22 return 0;
23 }
24
25 int main( void )
26 {
27 //从标准输入尝试读数据
28
29 SetNoBlack(0);
30 while(1){
31 sleep(1);
32 printf("> ");
33 fflush(stdout);
34 char buf[1024]={0};
35 ssize_t read_size=read(0,buf,sizeof(buf)-1);
36 if(read_size<0){
37 perror("read error");
38 continue;
39 }
40 if(read_size==0){
41 printf("read done!\n");
42 return 0;
43 }
44 buf[read_size]='\0';
45 printf("buf=%s\n",buf);
46 }
47 }
重定向
dup/dup2系统调用
将第二个数据重定向到第一个数据中
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
使用dup2将标准输出重定向到文件中
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("./log", O_CREAT | O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
for (;;) {
char buf[1024] = { 0 };
ssize_t read_size = read(0, buf, sizeof(buf) - 1); if (read_size < 0) {
perror("read");
continue;
}
printf("%s", buf);
fflush(stdout);
}
return 0;
}
select来实现多路复用输入/输出模型.
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
1. 参数nfds是需要监视的最大的文件描述符值+1;
2. rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
3. 参数timeout为结构timeval,用来设置select()的等待时间
fd_set
结构其实就是一个整数数组,更严格的说,是个“位图”。使用位图中对应的位来表示要监视的文件描述符.
timeval
结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
函数返回值:
1. 执行成功则返回文件描述词状态已改变的个数
2. 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
3. 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout 的值变成不可预测。
错误值可能为:
* EBADF 文件描述词为无效的或该文件已关闭
* EINTR 此调用被信号所中断
* EINVAL 参数n 为负值。
* ENOMEM 核心内存不足
1 /*
2 * 实现一个简单的标准输入监控程序,从标准输入拿到数据显示到终端
3 * 目的:熟悉一下select用法和功能
4 *
5 * select功能是对描述符集合中的描述符进行状态改变监控
6 * 当集合中有描述符就绪时,将返回
7 * 或者当select等待超时时,将返回
8 * 或者当select等待出错时,将返回
9 *
10 * int select(int nfds,fd_set readfds,writefds,exceptfds,tv);
11 * nfds:监控的最大描述符+1
12 * readfds:监控的可读描述符集合
13 * writefds:监控可写的描述符集合
14 * exceptfds:监控异常的描述符集合
15 * tv:select是一个阻塞调用,但是可以设置阻塞的时间
16 * NULL:一直阻塞
17 * 0: 非阻塞
18 * >0: 在指定在这段时间内如果没有描述符就绪,则返回0,超时
19
20 */
21 #include <stdio.h>
22 #include <unistd.h>
23 #include <stdlib.h>
24 #include <errno.h>
25 #include <string.h>
26 #include <sys/select.h>
27 #include <fcntl.h>
28 #include <time.h>
29 int main( void )
30 {
31 fd_set readfds;
32 struct timeval tv;
33 int max_fd=0;
34
35 while(1){
36 //设置select的等待超时时间
37 tv.tv_sec=3;
38 tv.tv_usec=0;
39 //清空可读事件集合,将标准输入加入集合中
40 FD_ZERO(&readfds);
41 //将标准输入的描述符加入到集合中
42 FD_SET(0,&readfds);
43 int ret=select(max_fd+1,&readfds,NULL,NULL,&tv);
44 if(ret<0){
45 perror("select error");
46 continue;
47 }
48 else if(ret==0){
49 printf("timeout!!\n");
50 continue;
51 }
52
53 char buff[1024]={0};
54 ret=read(0,buff,1023);
55 buff[ret-1]='\0';
56 printf("buff:[%s]\n",buff);
57 }
58 }
如果在3秒内标准输入没有输入,则超时
socket就绪条件
读就绪
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记
SO_RCVLOWAT
. 此时可以无阻塞的读该文件描述符, 并且返回值大于0; - socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求; socket上有未处理的错误
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记
写就绪
- socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
SO_SNDLOWAT
, 此时可以无阻塞的写, 并且返回值⼤大于0; - socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发
SIGPIPE
信号; - socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误
- socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
- 异常就绪
- socket收到异常数据(和TCP紧急模式相关)
select特点
- 可监控的文件描述符个数取决与sizeof(fd_set) 的值 .
这边服务器上 sizeof(fdset)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096.
- 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
一是用于再select 返回后,array作为源数据和fdset 进行 FDISSET判断。
二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数
select缺点
- 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小
poll
- 函数接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd
{
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
- 参数说明
fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.
nfds表示fds数组的长度.
timeout表示poll函数的超时时间, 单位是毫秒(ms)
- 返回结果
返回值小于0, 表示出错; 返回值等于0, 表示poll函数等待超时; 返回值大于0, 表示poll由于监听的文件描述符就绪而返回.
- pol缺点
poll中监听的文件描述符数目增多时
和select函数一样,poll返回后,需要 轮询 pollfd来获取就绪的描述符.
每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
//使用poll监控标准输入
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
struct pollfd poll_fd;
poll_fd.fd = 0; 、
poll_fd.events = POLLIN;
for (;;) {
int ret = poll(&poll_fd, 1, 1000);
if (ret < 0) {
perror("poll");
continue;
}
if (ret == 0) {
printf("poll timeout\n");
continue;
}
if (poll_fd.revents == POLLIN) {
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("stdin:%s", buf);
}
}
}