I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是 读就绪 或者 写就绪 ),能够通知程序进行相应的读写操作。
select 、 poll 和 epoll 是 Linux API 提供的 I/O 复用方式。.
本质上就是,把本应该由程序员的事情交给内核负责:检测这些读写缓冲区是否可用(需要调用阻塞函数);检测一轮以后内核将可操作的fd告诉我们,我们再去操作就不会再阻塞。
select和poll的底层是线性表
服务器的文件描述符
服务器包含两种文件描述符,每一个都有两个缓存区:
1、监听LFD:用于标志是否有数据是否能够读写数据,需要调用accept系统调用。(有且仅有一个)
2、通讯FD1:读取和输出数据,调用read/recv、write/send方法。(N,每建立一次连接加1个)
问题在于该三个方法(系统调用)都是阻塞的,当全部在一个线程里就会出现一个阻塞都阻塞。
使用多路IO复用,四个buffer就不需要自己去维护了;交由内核维护。
当内核检测之后会通知那些缓冲区 可以读写,再去系统调用,由于已经就绪不会再有堵塞发生。
当处理数据并不是同时进行,而是优先到后进行的(因为是单线程)。
select 概述
可跨平台
select 系统调用是用来让我们的程序**监视多个文件句柄的状态变化的** 。
select 函数监视的文件描述符分 3 类:writefds 、 readfds 、和 exceptfds 。
调用后 select 函数会阻塞 ,直到有描述符就绪(有数据可读 、可写 、或者有 except ),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回 。
缺点
select 本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1 、 单个进程可监视的 fd 数量被限制,即能监听端口的大小有限;
2 、 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有**O(n)**的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
3 、需要维护一个用来存放大量 fd 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
函数int select (int n, fd_set *readfds , fd_set *writefds , fd_set *exceptfds , struct timeval *timeout);
n:要检测的文件描述符集合里,最大的文件描述符的值+1;这是因为fd是线性表(数组)的下标;或者直接写1024。
readfds:读集合,检测一系列文件描述符的读缓冲区。
传入传出参数,读集合(放需要写的fd) 一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据。
writefds:写集合,检测一系列文件描述符的读缓冲区。
传入传出参数,如果不需要使用这个参数可以指定为NULL。
exceptfds :异常集合
slect函数检测这些集合里的文件描述符,如果这些集合都没有满足条件(读集合里fd的读缓存区全为空,写集合里fd的写缓冲区全为慢的)select会一直在检测;timeout设置最多检测时间;timeout=0,select函数不阻塞,直接退出。
为什么类型是fd_set *?
因为我们需要把一块内存传给select,在函数体内部修改这个内存;也就是缺点3。
占据1024bit,128Byte,int[32];=但是以bit去处理数据。
每一个文件标识符对应一个bit。
如果集合中标志位为 0代表不检测 这个fd状态;
如果集合中标志位为 1代表检测 这个fd状态;这个fd在fd_set里
fd3 6 9 10读缓存区有数据,记录下来并回传给用户空间;
遍历检查fs_set是否有为1的,为1的说明已经准备就绪,分辨一下是什么类型的缓存区。
void FD_CLR(int fd, fd_set *set); // 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1 int FD_ISSET(int fd, fd_set *set); // 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1 void FD_SET(int fd, fd_set *set); // 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符 void FD_ZERO(fd_set *set);select流程
- 先将需要监控读、写或异常的fd分别存入对应的集合中;
- 这三个集合会被拷入进内核中;内核基于一个线性表去轮询这些集合的事件;
- 内核发现出现了事件(可以读写或者出现异常),传出三个集合;
- 传出的三个集合会被内核写到传入的三个集合的地址上(readfds 、writefds 、exceptfds 被内核修改)。
举个栗子:
| 集合 | 传入 | 传出 |
|---|---|---|
| readfds | 5、6、7、8、9 | 5、6、7、8、9 |
| writefds | 7、8、9、10 | 9、10 |
| exceptfds | 5-10 | 空 |
传出和传入的集合在同一块内存上,由内核负责改写。
读集合中有5和6号描述符,判断一下到底是用于监听的还是数据的;如果是监听的调用accept,与客户端建立链接;读数据就是调用read/rev。
写集合有9、10,调用send。
- 初始化fd_set,服务器的套接字fd并绑定
- while(1)
- 运行select,并把select之后的fd_set提出来进行遍历,分辨是监听的还是数据的
- 如果是监听的,将新的fd加入fd_set中
初始化服务器套接字并绑定
int iSocketServer= socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(iSocketServer, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
struct sockaddr_in tSocketServerAddr;
struct sockaddr_in tSocketClientAddr;
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT);
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
memset(tSocketServerAddr.sin_zero,0,8);
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr,
sizeof(struct sockaddr));
//设置监听
listen(iSocketServer, 128);
iSocketServer为fd,套接字;tSocketServerAddr负责储存IP端口信息,并通过bind实现于iSocketServer绑定。
tSocketServerAddr主要有四块需要设置的:
- sin_family通讯类型;
- sin_port服务器端口,注意需要通过htons()转变为二进制;
- sin_addr.s_addr历史遗留问题。
- memset(tSocketServerAddr.sin_zero,0,8); 预留位置0。
服务器的设置
- iSocketServer = socket(AF_INET, SOCK_STREAM, 0); 得到文件描述符;
- struct sockaddr_in tSocketServerAddr; 包含IP和端口信息,主要初始化以下四个:
sin_family:链接类型;sin_port:客户端端口号;sin_addr.s_addr:客户端IP。 - bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr,
sizeof(struct sockaddr)); 绑定tSocketServerAddr和iSocketServer。
设置fd_set 集合
// 初始化检测的读集合
fd_set rdset;
fd_set rdtemp;
// 清零
FD_ZERO(&rdset);
// 将监听的lfd设置到检测的读集合中
FD_SET(iSocketServer, &rdset);
rdtemp主要用于拷贝使用,作为rdset的备份;
清空集合,并把iSocketServer加入集合中;由于目前集合只有一个iSocketServer,所以iSocketServer值必然是当前最大的。
while循环下的select
while(1){
rdtemp = rdset;
int num = select(maxfd+1, &rdtemp, NULL, NULL, NULL);
//select会修改rdtemp值,现在去检查集合的fd。
//因为用于监视的fd就只有一个,只用看iSocketServer现在还在不在集合里
if(FD_ISSET(iSocketServer, &rdtemp))
{
// 接受连接请求, 这个调用不阻塞
int cliLen = sizeof(tSocketClientAddr);
int iSocketClient = accept(iSocketServer,
(struct sockaddr*)&tSocketClientAddr, &cliLen);
// 得到了有效的文件描述符
// 通信的文件描述符添加到读集合
// 在下一轮select检测的时候, 就能得到缓冲区的状态
FD_SET(iSocketClient , &rdset);
// 重置最大的文件描述符
maxfd = cfd > maxfd ? cfd : maxfd;
}
for(int i=0; i
// 判断除监听用fd外,其他fd是否在集合中
if(i != lfd && FD_ISSET(i, &rdtemp))
{
char buf[10] = {0};
// 一次只能接收10个字节, 客户端一次发送100个字节
// 一次是接收不完的, 文件描述符对应的读缓冲区中还有数据
// 下一轮select检测的时候, 内核还会标记这个文件描述符缓冲区有数据 -> 再读一次
// 循环会一直持续, 知道缓冲区数据被读完位置
int len = read(i, buf, sizeof(buf));
if(len == 0)
{
printf("客户端关闭了连接...n");
// 将检测的文件描述符从读集合中删除
FD_CLR(i, &rdset);
close(i);
}
else if(len > 0)
{
// 收到了数据
// 发送数据
write(i, buf, strlen(buf)+1);
}
else
{
// 异常
perror("read");
}
}
}
}
1、accept(int sockfd,struct sockaddr *addr,socklen_t addrlen);
fd服务器套接字,addr用来存放客户端的IP端口信息,addrlen是addr的大小;其中(struct sockaddr) &sockaddr_in直接进行类型转换得到。
返回的客户端的fd,iSocketClient。
2、将新的客户端fd加入rdset集合中,更新集合;
3、 maxfd = cfd > maxfd ? cfd : maxfd; 更新最大的fd值。
4、如果缓冲区一次没有被读完,内核仍标记这个文件描述符缓冲区有数据,下一次依旧会被加入集合。
5、int len = read(i, buf, sizeof(buf)); 当len=0时,说明数据已经被读完了。
客户端代码
客户端不需要考虑select的问题,和普通的client一样即可
int iSocketClient;
iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
unsigned char ucSendBuf[1000];
struct sockaddr_in tSocketSever;
tSocketSever.sin_family = AF_INET;
tSocketSever.sin_port = htons(SERVER_PORT);
//int inet_aton(const char *cp,struct in_addr *inp)
//tSocketSeverAddr.sin_addr.s_addr = INADDR_ANY;
if( 0 == inet_aton(argv[1],&tSocketSever.sin_addr))
{
printf("invalid server_ip!n");
return -1;
}
else
printf("Server IP : %d n",tSocketSever.sin_addr.s_addr);
memset(tSocketSever.sin_zero,0,8);
// connect(iSocketClient, (const struct sockaddr *)&tSocketSever,
// sizeof(tSocketSever));
if( -1 == connect(iSocketClient, (const struct sockaddr *)&tSocketSever,
sizeof(tSocketSever)))
{
printf("connet error!n");
return -1;
}
while(1)
{
if(fgets(ucSendBuf, 999 ,stdin))
{
ssize_t iSendlen = send(iSocketClient, ucSendBuf,
strlen(ucSendBuf), 0);
if(iSendlen <= 0)
{
close(iSocketClient);
return -1;
}
}
}
1、iSocketClient = socket(AF_INET, SOCK_STREAM, 0); 设置客户端fd;
2、struct sockaddr_in tSocketSever; 定义所链接服务器的性质;
3、connect(iSocketClient, (const struct sockaddr *)&tSocketSever,sizeof(tSocketSever)); 申请链接;
select和poll差不多,但是由于不能跨平台,但是一般不使用。
和 select 函数一样,监听多个文件描述符,直到条件满足或超时的时候 poll 返回,通过 遍历文件描述符来获取已经就绪的描述符。
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.。
不同于 select 使用三个位图来表示三个 fdset 的方式, poll 使用 一个 pollfd 的指针实现。
缺点
缺点基本与 select 函数一致
1 . poll 返回后 通过遍历文件描述符来获取已经就绪的描述符 。
2 . 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降 。
概述#include// 每个委托poll检测的fd都对应这样一个结构体 struct pollfd { int fd; short events; short revents; }; struct pollfd myfd[n]; int poll(struct pollfd *fds, nfds_t nfds, int timeout);
event:说明需要内核监视什么事件;
revents:内核检测到事件(由event决定是什么事件)就会储存在revents;
struct pollfd myfd[n]; 每个文件描述符需要一个pollfd,所以当有多个n个文件描述符时候,需要有n个pollfd,索性写为数组。
nfds_t nfds:数组(最大的)有效元素下标+1;
timeout:最长阻塞时长;
返回值:准备就绪的文件描述符个数。
unsigned int (*poll) (struct file *, struct poll_table_struct *);
//返回值:event事件
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table * p);
typedef struct poll_table_struct {
poll_queue_proc _qproc;
unsigned long _key;
} poll_table;
//poll_table_struct 和 poll_table是同一个结构体
服务器代码
......
struct pollfd fds[1024];
// 初始化
for(int i=0; i<1024; ++i)
{
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int maxfd = 0;
while(1)
{
// 委托内核检测
ret = poll(fds, maxfd+1, -1);
if(ret == -1)
{
perror("select");
exit(0);
}
// 检测的度缓冲区有变化
// 有新连接
if(fds[0].revents & POLLIN)
{
// 接收连接请求
struct sockaddr_in sockcli;
int len = sizeof(sockcli);
// 这个accept是不会阻塞的
int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
// 委托内核检测connfd的读缓冲区
int i;
for(i=0; i<1024; ++i)
{
if(fds[i].fd == -1)
{
fds[i].fd = connfd;
break;
}
}
maxfd = i > maxfd ? i : maxfd;
}
// 通信, 有客户端发送数据过来
for(int i=1; i<=maxfd; ++i)
{
// 如果在集合中, 说明读缓冲区有数据
if(fds[i].revents & POLLIN)
{
char buf[128];
int ret = read(fds[i].fd, buf, sizeof(buf));
if(ret == -1)
{
perror("read");
exit(0);
}
else if(ret == 0)
{
printf("对方已经关闭了连接...n");
close(fds[i].fd);
fds[i].fd = -1;
}
else
{
printf("客户端say: %sn", buf);
write(fds[i].fd, buf, strlen(buf)+1);
}
}
}
}
close(lfd);
......
主要是对pollfd fds[1024]的处理:
1、全部fd置为-1,并设置events ,因为fd>0的;说明为-1的都是可用空位;
2、由于第一个肯定都是监听的,所以fds[0]=lfd;
3、ret = poll(fds, maxfd+1, -1) timeout:-1,会一直阻塞直到有数据;
4、通过for循环找到第一个为-1的位置,填充上新的链接的fd;
5、通过收发的read函数的返回值判断。
select和poll的问题
1、select需要给内核传递三个集合;一次select需要三个拷贝;
2、poll需要传递一整个pollfd[]的数组;
3、由于是线性检测,所以待检测的套接字越多越慢。



