前言:我们在上一讲 Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO,对于其中的 阻塞/非阻塞IO 进行了说明。
这一讲我们来看 多路复用机制。
所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要额外的功能来配合: select、poll、epoll。
select、poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
select时间复杂度O(n):它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
poll时间复杂度O(n):poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
epoll时间复杂度O(1):epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
在多路复用IO模型中,会有一个内核线程不断去轮询多个socket的状态,只有当真正读写事件发生时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
小编推荐自己的linuxC/C++语言技术交流群:【1106675687】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!
使用select来监视文件描述符时,要向内核传递的信息包括:
fd_set 模型的原理(理解select模型的关键在于理解fd_set,假设取fd_set长度为1字节):
fd_set中的每一位可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
执行FD_ZERO(&set);则set用位表示是0000,0000。
若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
若再加入fd=2,fd=1,则set变为0001,0011 、执行select(6,&set,0,0,0)阻塞等待…
若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)
1、使用copy_from_user从用户空间拷贝fd_set到内核空间
2、注册回调函数__pollwait
3、遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
4、以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
5、__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意:把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
6、poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
7、如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout 指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
8、把fd_set从内核空间拷贝到用户空间。
select的几大缺点:
1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
3)select支持的文件描述符数量太小了,默认是1024
/* According to POSIX.1-2001, POSIX.1-2008 */
#include
/* According to earlier standards */
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set); //清除某一个被监视的文件描述符。
int FD_ISSET(int fd, fd_set *set); //测试一个文件描述符是否是集合中的一员
void FD_SET(int fd, fd_set *set); //添加一个文件描述符,将set中的某一位设置成1;
void FD_ZERO(fd_set *set); //清空集合中的文件描述符,将每一位都设置为0;
#include
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
struct timeval{
long tv_sec; //秒
long tv_usec;//微秒
}
struct timespec{
time_t tv_sec;//秒
long tv_nsec;//纳秒
}
select和pselect有三个主要的区别:
1、select超时使用的是struct timeval,用秒和微秒计时,而pselect使用struct timespec ,用秒和纳秒。
2、select会更新超时参数timeout 以指示还剩下
sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL);
sigmask:这个参数保存了一组内核应该打开的信号(即:从调用线程的信号掩码中删除)
当pselect的sigmask 为 NULL时pselect和select一样;当sigmask!=NULL时,等效于以下原子操作:
sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL);
接收信号的程序通常只使用信号处理程序来引发全局标志。全局标志将指示事件必须被处理。在程序的主循环中。一个信号将导致select和pselect返回-1 并将erron=EINTR。
我们经常要在主循环中处理信号,主循环的某个位置将会检查全局标志,那么我们会问:如果信号在条件之后,select之前到达怎么办。答案是select会无限期阻塞。
这种情况很少见,但是这就是为什么出现了pselect。因为他是类似原子操作的。
描述: 允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种 。
参数说明:
注意, 待测试的描述集总是从0, 1, 2, …开始的。 所以, 假如你要检测的描述符为8, 9, 10, 那么系统实际也要监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是max(8, 9, 10) + 1
readfds/writefds/exceptfds:这些都是fd_set类型的,代表文件描述符集合; 可以认为一个fd_set变量是由很多个二进制构成的数组,每一位表示一个文件描述符是否需要监视。
如果timeout ->tv_sec 为0 且 timeout->tv_sec 为0 ,不等待直接返回,加入的描述符都会被测试,并且返回满足要求的描述符个数,这种方法通过轮询,无阻塞地获得了多个文件描述符状态。
如果timeout->tv_sec!=0 || timeout->tv_sec!=0 ,等待指定的时间。当有描述符复合条件或者超过超时时间的话,函数返回。等待总是会被信号中断。
返回值
server.c
/*
# Copyright By Schips, All Rights Reserved
# https://gitee.com/schips/
#
# File Name: server.c
# Created : Sat 25 Mar 2020 14:43:39 PM CST
*/
#include
#include
#include
#include
#include /* See NOTES */
#include
#include
#include
typedef struct _info {
char name[10];
char text[54];
}info;
int main(int argc, char *argv[])
{
int my_socket;
unsigned int len;
int ret, i, j;
// 创建套接字
my_socket = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP socket
if(my_socket == -1) {
perror("Socket"); }
printf("Creat a socket :[%d]\n", my_socket);
// 用于接收消息
info buf ={
0};
// 指定地址
struct sockaddr_in addr = {
0};
addr.sin_family = AF_INET; // 地址协议族
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //指定 IP地址
addr.sin_port = htons(12345); //指定端口号
int set = 1;
int get = 0;
int getlen = 0;
// 服务器 绑定
bind(my_socket, (struct sockaddr *)&addr, sizeof(addr));
ret = listen(my_socket, 10);
if(-1 == ret) {
perror("listen"); }
printf("Listening\n");
int connect_sockets[100] = {
0}; // 我们规定,为 0 的成员为无效socket
int connected_cnt = 0;
//struct sockaddr_in new = {0};
//int new_addr_size = {0};
fd_set read_sets;
int max_fd = my_socket; // 一开始时,只有 一个新的 文件描述:my_socket ,所以它是最大的
while(1) // 在循环中等待连接请求
{
FD_ZERO(&read_sets); // 每次都需要初始化
FD_SET(my_socket, &read_sets); // 添加 要监听的 socket
// 添加 之后经过 connect 过来的 套接字数组(一般在第一次循环时是空的)
for( i = 0; i < connected_cnt; i++)
{
if(connect_sockets[i])
{
FD_SET(connect_sockets[i], &read_sets); // 添加经过accept保存下来,需要进行读响应的套接字到集合中
}
}
// 设置监听超时时间
// timeout.tv_sec = 2;
// timeout.tv_usec = 0;
ret = select(max_fd + 1, &read_sets, NULL, NULL, NULL);
// 判断返回值
switch (ret) {
case 0 :
printf("Time out.\n"); // 监听超时
break;
case -1 :
printf("Err occurs.\n"); // 监听错误
break;
default :
if(FD_ISSET(my_socket, &read_sets)) //这个是原的被动socket,如果是它,则 意味着有新的连接进来了
{
connect_sockets[connected_cnt] = accept(my_socket, NULL, NULL);
max_fd = connect_sockets[connected_cnt];
printf("New socket is %d\n", connect_sockets[connected_cnt]);
connected_cnt ++;
printf("Now we has [%d] connecter\n", connected_cnt);
}else{
// 如果不是 被动socket,那么就意味着是 现有的连接 有消息发来(我们有数据可读)
printf("New message came in.\n");
// 求出是那个文件描述符可读
for(i = 0; i < connected_cnt; i++)
{
if(FD_ISSET(connect_sockets[i], &read_sets) == 1) break;
}
if( i >= connected_cnt) {
continue; }
printf("Socket [%d] send to server.\n", connect_sockets[i]);
// 接收消息
ret = recv(connect_sockets[i], &buf, sizeof(buf), 0);
if( ret <= 0 )
{
// 远程客户端断开处理(如果不处理,会导致服务器也断开)
printf("[%d]/[%d] Client [%d] disconnected.\n", i+1, connected_cnt, connect_sockets[i]);
close(connect_sockets[i]);
// 我们需要将对应的客户端从数组中移除 且 连接数 -1 (移除的方法: 数组成员前移覆盖)
for (j = i; j < connected_cnt - 1; ++j)
{
connect_sockets[j] = connect_sockets[ j + 1];
}
connected_cnt --;
}
// 打印消息
printf("[%s] : %s\n", buf.name, buf.text);
// 回复消息
sprintf(buf.name, "Server");
sprintf(buf.text, "Had recvied your[%d] message", connect_sockets[i]);
//sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
send(connect_sockets[i], &buf, sizeof(buf), 0);
}
break;
}
printf("while loop\n");
}
// 关闭连接
//shutdown(my_socket, SHUT_RDWR); perror("shutdown");
for(i = 0; i < connected_cnt; i++)
{
close(connect_sockets[i]); perror("close");
}
return close(my_socket);
}
**client.c**
``/*
# Copyright By Schips, All Rights Reserved
# https://gitee.com/schips/
#
# File Name: client.c
# Created : Sat 25 Mar 2020 14:44:19 PM CST
*/
#include
#include
#include
#include /* See NOTES */
#include
#include
#include
typedef struct _info {
char name[10];
char text[54];
}info;
int main(int argc, char *argv[])
{
int my_socket;
unsigned int len;
int ret, i = 0;
// 创建套接字
my_socket = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP socket
if(my_socket == -1) {
perror("Socket"); }
printf("Creat a socket :[%d]\n", my_socket);
// 用于接收消息
info buf ={
0};
// 指定地址
struct sockaddr_in addr = {
0};
addr.sin_family = AF_INET; // 地址协议族
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //指定 IP地址
addr.sin_port = htons(12345); //指定端口号
int new_socket;
struct sockaddr_in new = {
0};
int new_addr_size;
connect(my_socket, (struct sockaddr *)(&addr), sizeof(struct sockaddr_in));
if(-1 == ret) {
perror("connect"); }
printf("connected\n");
// 回复消息
sprintf(buf.name, "Client");
sprintf(buf.text, "Hello tcp text.");
//sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
send(my_socket, &buf, sizeof(buf), 0);
perror("sendto");
// 接收并打印消息
//recvfrom(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
recv(my_socket, &buf, sizeof(buf), 0);
perror("recvfrom");
printf("[%s] : %s\n", buf.name, buf.text);
for (i = 0; i < 5; ++i)
{
sleep(2);
// 回复消息
sprintf(buf.name, "Client");
sprintf(buf.text, "Hello tcp text [%d].", i++);
//sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
send(my_socket, &buf, sizeof(buf), 0);
perror("sendto");
// 接收并打印消息
//recvfrom(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
recv(my_socket, &buf, sizeof(buf), 0);
printf("[%s] : %s\n", buf.name, buf.text);
perror("recvfrom");
}
// 关闭连接
//shutdown(my_socket, SHUT_RDWR); perror("shutdown");
//printf("%d\n", errno);
return close(my_socket); perror("close");
printf("%d\n", errno);
return errno;
}