1、阻塞IO和非阻塞IO
1.1 阻塞IO
- 在阻塞IO模型中,当一个IO操作(如读取或写入)开始时,如果数据没有准备好,程序会被挂起(即阻塞),直到数据准备好并且IO操作完成。
- 在数据准备阶段,程序做不了其他事情,它会等待在那里直到整个IO操作完全完成后才会继续执行。
- 阻塞IO简化了编程模型,因为在发起IO请求之后,你只需要等待结果,不必担心在这个过程中如何处理其他任务。
1.2 非阻塞IO
- 非阻塞IO模型中,IO操作不会导致程序挂起等待。如果数据没有准备好,IO调用会立即返回一个指示数据尚未准备好的状态。
- 程序可以继续执行后续代码,它可以定期地检查IO操作是否可以进行,或者可以进行其他工作,这样很好地利用了等待时间。该过程称为 polling 。
- 非阻塞IO通常与事件驱动或轮询机制一起使用,程序可以在多个IO操作之间有效地切换,提高了程序的整体效率和响应性。
非阻塞IO更适用于处理高并发情况,尤其在需要同时管理多个连接或者进行多个并行IO操作时。但这种模型编程难度较高,因为要管理和调度多个IO操作,而且可能需要额外的机制(如IO复用或事件通知)来检测IO状态。
1.3 如何将阻塞IO更改成非阻塞IO
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能:获取或者设置当前文件描述符的属性和状态,是哪一种操作取决于cmd
fd:要处理的文件描述符
cmd: 要执行的操作
F_GETFL: 获取文件描述符的属性,第三个参数可以忽略
F_SETFL:设置文件描述符的属性,第三个参数为int
是否设置参数3,取决于参数2,如果参数2为 F_SETFL,参数3就是为当前文件描述符设置的值
返回值:取决于cmd
如果cmd 为F_GETFL,则成功返回当前文件描述符的属性值,失败返回-1并置位错误码
如果cmd 为 F_SETFL,则成功返回0,失败返回-1并置位错误码
步骤:
1、先获取当前文件描述符的属性
int flag = fcntl(fd, F_GETFL);
2、将当前的文件描述符的属性中,添加非阻塞属性
flag |= O_NONBLOCK;
3、将更改后的状态加到当前文件描述符中
fcntl(fd, F_SETFL, flag);
int main(int argc, const char *argv[]) { int num = 0; //1、获取当前文件描述的属性 int flag = fcntl(0, F_GETFL); //2、给当前的文件描述符加上非阻塞属性 flag |= O_NONBLOCK; //3、再将新属性设置回去 fcntl(0, F_SETFL, flag); while(1) { printf("请输入:"); scanf("%d", &num); //此处省略一万行代码 printf("num = %d\n", num); } return 0; }
2、IO多路复用
2.1 IO多路复用的相关概念
1、如果程序中需要同时处理多个阻塞任务时,在使用单进程或单线程的情况下,可以处理多个阻塞任务
2、在无法使用多进程或多线程时,还想完成多任务并发,此时可以使用IO多路复用技术
3、优点:由于不需要多进程或多线程,能够有效减少系统的资源开销,减少上下文切换的次数,效率较高
4、原理图
2.2 select
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:阻塞等待检测容器中是否有事件产生,如果有一个或多个事件产生,则解除阻塞,并且将没有产生事件的文件描述符从集合中清除
参数1:检测集合中最大的文件描述符的值 + 1
参数2、3、4:分别是读、写、异常检测的文件描述符集合,如果不用,可以填NULL
参数5:超时时间,可以是如下的结构体变量的地址,也可以填NULL,表示永久等待
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
and
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
返回值:
>0 表示本次解除阻塞触发事件的文件描述符总个数
=0 表示设置的超时时间,并超时时间内没有事件产生
=-1 出错,置位错误码
四个宏函数,用于对文件描述符集合的操作
void FD_CLR(int fd, fd_set *set); // 将fd文件描述符从集合set中移除
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实现TCP并发服务器--框架
int main(){
sfd = socket(); // 创建用于连接的套接字
bind(); // 绑定ip和端口号
listen(); // 将套接字设置成被动监听状态
fd_set readfds, tempfds;
FD_ZERO(&readfds)
FD_SET(sfd, &readfds);
int max = sfd;
while(1){
tempfds = readfds;
select(max+1, &tempfds, NULL, NULL,NULL); //阻塞检测集合中文件描述符
是否有事件产生
for(int i = 0; i <= max; i++){
if(!FD_ISSET(i, &tempfds)){
continue;
}
if(i == sfd){
newfd = accept()
FD_SET(newfd ,&readfds );
max = max > newfd ? max : newfd;
}else{
recv()
send()
(下线关闭)close(i)
FD_CLR(i, &readfds);
//更新max
for(j = max; j >= sfd; j--){
if(FD_ISSET(j, &tempfds)){
max = j;
}
}
}
}
}
close(sfd);
}
2.3 poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:阻塞等待文件描述符集合中是否有事件产生,如果有事件产生,则解除阻塞
参数1:文件描述符集合数组,是一个结构体数组
struct pollfd {
int fd; /* 要检测的文件描述符 */
short events; /* 要检测的事件 */
short revents; /* 真实发生的事件 */ 该值无需初始化,poll函数会给
该参数赋值
};
常用事件种类:
POLLIN:读事件
POLLOUT:写事件
参数2:集合中文件描述符的个数
参数3:以毫秒为单位的超时时间
>0: 表示设置的超时时间
=0: 表示非阻塞
<0: 表示永久阻塞
返回值:
>0: 表示返回解除阻塞的事件对应的文件描述符个数
=0: 表示超时
<0: 失败,置位错误码
poll完成TCP客户端中发送数据和接收数据的并发执行
int main(){
cfd = socket();
bind()
connect();
// 使用poll 完成 多阻塞任务的并发
struct pollfd pfd[2]; // 定义一个2长度的结构体数组
pfd[0].fd = 0; // 表示检测0号(stdin)文件描述符
pfd[0].events = POLLIN; // 表示检测读事件
pfd[1].fd = cfd; // 表示检测cfd文件描述符
pfd[1].events = POLLIN; // 表示检测读事件
while(1){
int res = poll(pfd, 2, -1);
if (res == -1){
perror("poll error");
return -1;
}else if (res == 0){
printf("time out\n");
return -1;
}
//判断是否为0号文件描述符产生的事件
if(pfd[0].revents == POLLIN){
}
//判断是否为cfd文件描述符产生事件
if(pfd[1].revents == POLLIN){
recv()
}
}
close(cfd)
return 0;
}