小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
1 IO模型
1.1 分类
在UNIX/Linux下主要有4种I/O 模型:
- 阻塞I/O:最常用、最简单、效率最低
- 非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
- I/O 多路复用:允许同时对多个I/O进行控制
- 信号驱动I/O:一种异步通信模型
1.2 阻塞IO
几乎所有的阻塞函数默认都是阻塞IO,
以读阻塞为例,
如果要读取的缓冲区中有数据,则正常执行
如果缓冲区中没有数据,则读函数会一直阻塞等待,当有数据的时候,==内核将会自动唤醒当前进程==,接着执行读操作
以写阻塞为例,
一般写操作是不会阻塞,只有当写操作对应的缓冲区写满时,会发生阻塞
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
char buf[32] = {0};
while(1)
{
fgets(buf, 32, stdin);
//标准输入都有文件描述符0,1,2
//没有这三个也可以操作,终端是设备文件
sleep(1);
printf("***************************\n");
}
return 0;
}
1.3 非阻塞IO
如果将一个函数设置为非阻塞,意味着:
- 如果要操作的缓冲区中有数据,则正常执行
- 如果要操作的缓冲区中没有数据,则当前函数立即返回(当前函数执行失败),接着执行下面的代码
当一个应用程序使用了非阻塞模式套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称作polling),当应用程序不停地polling内核来检查是否I/O操作已经就绪,这是非常浪费CPU的资源的。
有一部分函数自带标志位,可以设置非阻塞,但是大多数函数都无法直接设置非阻塞,需要通过一些函数来设置
使用fcntl函数设置非阻塞IO
-
WNOHANG
-
MSG_DONTWAIT
-
O_NONBLOCK
- 头文件:
#include <unistd.h>#include <fcntl.h>
- 原型:
int fcntl(int fd, int cmd, ... /* arg */ ); - 功能:操作一个文件描述符
- 参数:
- fd:文件描述符
- cmd:命令选项
- F_GETFL 获取文件状态标志位
- F_SETFL 设置文件状态标志位
- O_NONBLOCK 非阻塞
- ...arg:可变参,是否需要由cmd后面括号里面内容决定,如果是int就需要,如果是void就不需要
- 返回值:
- 成功: F_GETFL 文件状态标志位 F_SETFL 0
- 失败:-1
读改写:一位一位的改
-
第一步:读,获取之前标志位
-
第二步:改,改变标志位
-
第三步:写,将改后的标志位设置回去
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
char buf[32] = {0};
//使用fcntl函数实现非阻塞
//注意:对于标志位的操作,必须遵循只操作指定标志位而不改变其他标志位
//所以对寄存器或者位的操作,一般执行读改写三步
//第一步:读,获取之前标志位
int flags;
if((flags = fcntl(0, F_GETFL)) == -1)
{
perror("fcntl error");
exit(1);
}
//第二步:改,改变标志位
flags |= O_NONBLOCK;
//第三步:写,将改后的标志位设置回去
if(fcntl(0, F_SETFL, flags) == -1)
{
perror("fcntl error");
exit(1);
}
while(1)
{
if(fgets(buf, 32, stdin) == NULL)
{
perror("fgets error");
}
sleep(1);
printf("buf = [%s]\n", buf);
printf("***************************\n");
}
return 0;
}
2 IO多路复用
❓当一个代码中有多个阻塞函数时,因为代码默认都有先后执行顺序,所以无法做到每一个阻塞函数独立执行,相互没有影响,如何解决这个问题?
- 如果按照默认阻塞形式,无法解决;
- 如果设置为非阻塞,每一个函数都轮询查看缓冲区中是否有数据,可以解决这个问题,但是轮询比较消耗CPU资源,所以也不推荐;
- 如果使用多进程或者多线程,需要考虑资源释放问题,也不推荐。
与多线程、多进程相比,I/O多路复用系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。
- 处理多个描述符
- 服务器要处理多个服务或者多个协议
🀄相对比较好的方法是使用IO多路复用
IO多路复用的基本思想是:
- 先构造一张有关描述符的表,保存要操作的文件描述符;
- 然后调用一个函数,阻塞等待文件描述符准备就绪;
- 当有文件描述符准备就绪;
- 则函数立即返回;
- 执行相应的IO操作。
调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。
2.1使用select实现IO多路复用
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:允许一个程序操作多个文件描述符,阻塞等待文件描述符
准备就绪,如果有文件描述符准备就绪,函数立即返回,
执行相应的IO操作
参数:
nfds:最大的文件描述符加1
readfds:保存读操作文件描述符的集合
writefds:保存写操作文件描述符的集合
exceptfds:保存其他或者异常的文件描述符的集合
timeout:超时
NULL 阻塞
返回值:
成功:准备就绪的文件描述符的个数
失败:-1
清空集合set
void FD_ZERO(fd_set *set);
将文件描述符fd添加到集合set中
void FD_SET(int fd, fd_set *set);
将文件描述符fd从集合set中移除
void FD_CLR(int fd, fd_set *set);
判断文件描述符fd是否在集合set中
int FD_ISSET(int fd, fd_set *set);
返回值:
存在:1
不存在:0
2.1.2 服务器
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {0};
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
//注意:ip地址不能随便写,服务器在那个主机中运行,ip地址就是这个主机的
//如果是自己主机的客户端服务器测试,可以使用127网段的
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
//使用select实现IO多路服用
//第一步:创建一个保存要操作的文件描述符集合并清空
fd_set readfds;
FD_ZERO(&readfds);
int maxfd = sockfd;
while(1)
{
//第二步:将要操作的文件描述符添加到集合中
FD_SET(0, &readfds);
FD_SET(sockfd, &readfds);
//第三步:调用select函数,阻塞等待文件描述符准备就绪
if(select(maxfd+1, &readfds, NULL, NULL, NULL) == -1)
{
ERRLOG("select error");
}
//第四步:如果有文件描述符准备就绪,则select函数立即返回执行对应的IO操作
//注意:如果有文件描述符准备就绪,select函数返回之后,会自动将集合中没有准备就绪的文件描述符移除
//所以select函数返回之后,判断哪个文件描述符还在集合中,在的就是准备就绪的
if(FD_ISSET(0, &readfds) == 1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
printf("buf = %s\n", buf);
}
if(FD_ISSET(sockfd, &readfds) == 1)
{
if(accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen) == -1)
{
ERRLOG("accept error");
}
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
}
}
return 0;
}
2.1.3客户端
//TCP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:给服务器发送客户端的连接请求
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("connect error");
}
//进行通信
char buf[N] = {0};
while(1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
if(send(sockfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端退出了\n");
exit(0);
}
memset(buf, 0, N);
if(recv(sockfd, buf, N, 0) == -1)
{
ERRLOG("recv error");
}
printf("服务器:%s\n", buf);
}
return 0;
}
2.2poll
1.6.1 poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:同select,阻塞等待文件描述符准备就绪,如果有文件描述符
准备就绪,则函数立即返回并执行相应的IO操作
参数:
fds:结构体数组,有多少个元素由要操作的文件描述符的个数决定
struct pollfd {
int fd; 文件描述符
short events; 请求的事件
POLLIN 有数据可读
short revents; 返回的事件
};
nfds:要操作的文件描述符的个数
timeout:超时检测
>0 设置超时毫秒数
0 非阻塞
<0 阻塞
返回值:
成功:准备就绪的文件描述符的个数
失败:-1