一:四种IO模
1.1:阻塞式IO(最简单,最常用,效率最低)
阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O 模式。
学习的读写函数在调用过程中会发生阻塞。相关函数如下:
•读操作中的read、recv、recvfrom
读阻塞--》需要读缓冲区中有数据可读,读阻塞解除
•写操作中的write、send
写阻塞--》阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。
注意:sendto没有写阻塞
1.2:非阻塞式IO(可以处理多路IO,需要轮询)
1.2.1:非阻塞式IO的设置
1)通过函数参数设置
Recv函数最后一个参数写为0,为阻塞,写为MSG_DONTWAIT:表示非阻塞
1.2.2:通过fctnl函数设置
int fcntl(int fd, int cmd, ... /* arg */ );
功能:获取/设置文件描述符属性 状态属性(O_RDONLY O_NONBLOCK非阻塞)
参数:fd:文件描述符
cmd:功能选择
状态属性:
F_GETFL :获取文件描述符原来的属性
F_SETFL :设置文件描述符属性
arg:根据cmd决定是否填充值 int
返回值:
失败:-1
成功:F_GETFL - 返回值的文件描述符号属性的值 int
F_SETFL 0
int flag;
flag=fcntl(0,F_GETFL);//获取文件描述符原属性
flag |= O_NONBLOCK;//添加非阻塞属性
// flag &= ~O_NONBLOCK;//取消非阻塞属性
fcntl(0,F_SETFL,flag);//将新属性设置回去
二:信号驱动IO(异步IO)
特点:异步通知模式,需要底层驱动的支持
//将APP进程号告诉驱动程序
fcntl(fd, F_SETOWN, getpid());
//使能异步通知
int flag;
flag = fcntl(fd,F_GETFL);
flag|= O_ASYNC ;
fcntl(fd,F_SETFL,flag);
signal(SIGIO,handler)
例子:鼠标键盘事件:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
int fd;
void handler(int num) // 信号处理
{
char buf[128]={};
int ret = read(fd,buf,sizeof(buf)-1);
buf[ret]='\0';
printf("buf:%s\n",buf);
}
int main(int argc, char const *argv[])
{
// 打开文件
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open err");
return -1;
}
// 将进程号告诉驱动
fcntl(fd, F_SETOWN, getpid());
// 开启异步通知
int flag;
flag = fcntl(fd,F_GETFL);
flag |= O_ASYNC;
fcntl(fd,F_SETFL,flag);
// 收到信号,调用函数
signal(SIGIO,handler);
while (1)
{
printf("welcome to hqyj\n");
sleep(1);
}
return 0;
}
三:IO多路复用
3.1:实现方式
3.1.1:select函数
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:select用于监测是哪个或哪些文件描述符产生事件;
参数:nfds: 监测的最大文件描述个数
(这里是个数,使用的时候注意,与文件中最后一次打开的文件
描述符所对应的值的关系是什么?)
readfds: 读事件集合; //读(用的多)
writefds: 写事件集合; //NULL表示不关心
exceptfds:异常事件集合;
timeout:超时检测
如果不做超时检测:传 NULL
如果设置了超时检测时间:&tv
返回值:
<0 出错
>0 表示有事件产生;
==0 表示超时时间已到;
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_CLR(int fd, fd_set *set);//将fd从表中清除
int FD_ISSET(int fd, fd_set *set);//判断fd是否在表中
void FD_SET(int fd, fd_set *set);//将fd添加到表中
void FD_ZERO(fd_set *set);//清空表
3.1.2:实现流程
3.1.3:select并发式服务器的实现
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
char buf[1024] = {};
int main(int argc, char const *argv[])
{
// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket create err\n");
return -1;
}
// 填充结构体
struct sockaddr_in addr, caddr;
addr.sin_family = AF_INET; // IPV4
addr.sin_port = htons(8881); // 主机字节序转换为网络字节序
addr.sin_addr.s_addr = inet_addr("192.168.31.88");
int len = sizeof(caddr);
// 绑定
int ret;
if (ret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("bind err\n");
return -1;
}
// 监听
if (listen(sockfd, 5) < 0)
{
perror("listen err\n");
return -1;
}
// 创建表
fd_set readfds, tempfd;
FD_ZERO(&readfds);
// 讲关心的文件描述符添加到表中
FD_SET(0, &readfds);
FD_SET(sockfd, &readfds);
int maxfd = sockfd;
while (1)
{
tempfd = readfds;
int ret = select(maxfd + 1, &tempfd, NULL, NULL, NULL);
if (FD_ISSET(0, &tempfd))
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
printf("key:%s\n", buf);
// 下面的文件描述符发生变化时,需要将数组里存储的内容重新发送
for (int i = sockfd + 1; i <= maxfd; i++)
{
// 判断其有没有在表中
if (FD_ISSET(i, &readfds))
{
send(i, buf, sizeof(buf), 0);
}
}
}
if (FD_ISSET(sockfd, &tempfd))
{
// 任何客户端都可以 ,返回通信文件描述符
int acceptfd = accept(sockfd, (struct sockaddr *)&addr, &len);
if (acceptfd < 0)
{
perror("accept err\n");
return -1;
}
// 来电显示
printf("通信文件描述符:%d\t", acceptfd);
printf("客户端端口:%d\t客户端ip:%s\n", ntohs(addr.sin_port), inet_ntoa(addr.sin_addr));
// close(acceptfd);
// 重新创表
FD_SET(acceptfd, &readfds);
// 更新文件描述符
if (maxfd < acceptfd)
{
maxfd = acceptfd;
}
}
for (int i = 4; i <= maxfd; i++)
{
if (FD_ISSET(i, &tempfd))
{
int rec = recv(i, buf, sizeof(buf), 0);
if (rec < 0)
{
perror("recv err\n");
return -1;
}
else if (rec == 0)
{
printf("i==客户端退出:%d\n", i);
// break;
// 链接关闭之后,关闭其所在的文件描述符,用时
close(i);
FD_CLR(i, &readfds);
if (i == maxfd)
{
maxfd--;
}
exit(1);
}
else
{
printf("%s\n", buf);
memset(buf, 0, sizeof(buf));
}
}
}
}
// 关闭文件描述符
close(sockfd);
return 0;
}
select实现io多路复用的特点:
- 一个进程最多只能监听1024个文件描述符 (千级别)FD_SETFILE
- select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源);
- select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低
3.2:poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
struct pollfd *fds
关心的文件描述符数组struct pollfd fds[N];
nfds:个数
timeout: 超时检测
毫秒级的:如果填1000,1秒
如果-1,阻塞
struct pollfd {
int fd; /* 检测的文件描述符 */
short events; /* 检测事件 */
short revents; /* 调用poll函数返回填充的事件,poll函数一旦返回,将对应事件自动填充结构体这个成员。只需要判断这个成员的值就可以确定是否产生事件 */
};
事件: POLLIN :读事件
POLLOUT : 写事件
POLLERR:异常事件
3.2.1:poll函数与select函数的区别
流程 | select | poll |
1.建立一个文件描述符的表 | fd_set线性表 | struct pollfd fds[n]结构体数组 |
2.将关心的文件描述符加到表中 | FD_SET(fd,&readfds) | 结构体内容填充fds[m].fd= fd fds[m].events=POLLIN |
3. 然后调用一个函数。 select / poll 4. 当这些文件描述符中的一个或多个已准备好进行I/O操作的时候 该函数才返回(阻塞)。 | select | poll |
5.判断 | FD_ISSET | revents==POLLIN |
6.相关操作 |
3.2.2:函数实现
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
char buf[1024] = {};
int main(int argc, char const *argv[])
{
int fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open faild\n");
return -1;
}
// 创建结构体数组
struct pollfd fds[1024] = {};
// 添加文件描述符
fds[0].fd = 0;
fds[0].events = POLLIN;
fds[1].fd = fd;
fds[1].events = POLLIN;
int last = 1;
while (1)
{
poll(fds,last+1, -1);
for (int i = 0; i <= last; i++)
{
if (fds[i].revents == POLLIN)
{
if (fds[i].fd == 0)
{
// scanf("%s", buf);
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
{
buf[strlen(buf) - 1] = '\0';
}
printf("。。。。。。:%s\n", buf);
}
if (fds[i].fd == fd)
{
read(fd, buf, sizeof(buf));
printf(".......:%s\n", buf);
}
}
}
}
return 0;
}
3.2.3:特点
优化文件描述符个数的限制;(根据poll函数第一个参数来定,如果监听的事件为1个,则结构体数组元素个数为1,如果想监听100个,那么这个结构体数组的元素个数就为100,由程序员自己来决定)
poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低
poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可
3.3:epoll函数
特点:
- 监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统)
- 异步I/O,Epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高
- epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.