Linux系统编程-->高效并发服务器:IO多路复用详解

目录

迭代服务器(socket+bind+listen)

并发服务器(socket+bind+listen)

IO多路复用

阻塞IO

特点

非阻塞IO

特点

信号驱动IO

使用流程

特点

IO多路复用

IO多路复用流程

select函数

功能

参数

返回值

select使用流程

注意

select存在的问题

select实现服务器并发

epoll函数

epoll_create

功能

参数

返回值

epoll_ctl函数

功能

参数

​编辑

返回值

epoll_wait

功能

参数

返回值


迭代服务器(socket+bind+listen)

特点:

  • 实现简单;
  • 可以处理多个客户端,但是只能是串行的

并发服务器(socket+bind+listen)

多进程存在的问题:

  • 资源开销大(创建的开销、调度的开销比较大);
  • 进程的退出(僵尸态资源的回收)

多线程相比于进程:

  • 线程的创建与调度开销小
  • 线程共享资源方便
  • 线程之间的竞争还有同步的问题

IO多路复用

核心目的:提高并发的程度

多路IO ------ I(input)O(output) 当前进程有多处输入和输出的操作

复用 ------

阻塞IO

scanf、getchar、fgets

阻塞IO

读取时

写入时(管道)

特点
  • 当读取数据时,内核没有数据,这种读取操作会一直阻塞,直到内核有数据
  • 实现简单,但是一直等效率不是很高

非阻塞IO

特点
  • 当去内核读取数据时,如果内核没有数据则不阻塞等待而是立即返回,所以非阻塞方式如果想要获得数据必须配合轮询操作
  • 如果是非阻塞,必须要while(1),很耗cpu
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, const char *argv[])
{

	if (argc != 2)
	{
		printf("Usage: %s <fifo file>\n",argv[0]);
		return -1;
	}

	if (mkfifo(argv[1],0666) < 0 && errno!= EEXIST)
	{
		perror("mkfifo fail");
		return -1;
	}

	printf("success\n");


	//1.打开文件 
    //int fd = open(argv[1],O_RDONLY);
	int fd = open(argv[1],O_RDONLY|O_NONBLOCK);
	if (fd < 0)
	{
		perror("open fail");
		return -1;
	}
#if 0
	//将fd设置为非阻塞
	int flag = fcntl(fd,F_GETFL,0);
	flag = flag | O_NONBLOCK;
	fcntl(fd,F_SETFL,flag);
#endif
	printf("read -------\n");
	while (1)
	{
		//2.写
		char buf[1024] = {0};
		int ret = read(fd,buf,sizeof(buf));
		perror("read test");
		printf("ret :%d buf = %s\n",ret,buf);
	    
		if (strncmp(buf,"quit",4) == 0)
		{
			break;
		}
		sleep(1);
	
	}
	//3.关闭 
	close(fd);
	if (unlink(argv[1]) < 0)
	{
		perror("unlink fail");
		return -1;
	}
	return 0;
}

信号驱动IO

  • 设置好对SIGIO的信号处理函数,等有数据来,内核会给进程发信号,进程收到信号后再去做读取操作
  • 不需要一直等,也不需要轮询
使用流程
  1. 为内核发送的通知信号安装一个信号处理例程,默认情况下这个通知信号为SIGIO
  2. 设定文件描述符的属主,也就是当文件描述符上可执行I/O时会接收到通知信号的进程或进程组。通常我们让调用进程称为属主,设定属主可通过fcntl()的F_SETOWN操作来完成
  3. 设置标志,通过设定O_NONBLOCK标志能使非阻塞IO,通过打开O_ASYNC标志能使信号驱动IO
特点

异步的,效率高,但是存在的问题是只能处理一路

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
int fd;
void do_handler(int signo)
{
	char buf[1024]={0};
	int ret = read(fd,buf,sizeof(buf));
	perror("read test");
	printf("ret :%d buf :%s\n",ret,buf);
	if(strncmp(buf,"quit",4)==0)
	{
		exit(0);
	}

}
int main(int argc, const char *argv[])
{

	if (argc != 2)
	{
		printf("Usage: %s <fifo file>\n",argv[0]);
		return -1;
	}

	if (mkfifo(argv[1],0666) < 0 && errno!= EEXIST)
	{
		perror("mkfifo fail");
		return -1;
	}

	printf("success\n");

	//1.设置信号处理函数
	signal(SIGIO,do_handler);

	//1.打开文件 
    //int fd = open(argv[1],O_RDONLY);
	int fd = open(argv[1],O_RDONLY|O_NONBLOCK);
	if (fd < 0)
	{
		perror("open fail");
		return -1;
	}

	//设置fd所属者
	fcntl(fd,F_SETOWN,getpid());

#if 0
	//将fd设置为非阻塞
	int flag = fcntl(fd,F_GETFL,0);
	flag = flag | O_NONBLOCK;
	fcntl(fd,F_SETFL,flag);
#endif
	printf("read -------\n");
	while (1)
	{
		//2.写
		char buf[1024] = {0};
		int ret = read(fd,buf,sizeof(buf));
		perror("read test");
		printf("ret :%d buf = %s\n",ret,buf);
	    
		if (strncmp(buf,"quit",4) == 0)
		{
			break;
		}
		sleep(1);
	
	}
	//3.关闭 
	close(fd);
	if (unlink(argv[1]) < 0)
	{
		perror("unlink fail");
		return -1;
	}
	return 0;
}

IO多路复用

IO多路复用流程
  1. 用函数(select/ epoll(poll))监控多路IO
  2. 如果哪一路IO有消息,则对应的函数返回告诉用户进程有就绪IO
  3. 则用户进程去对应的IO读取数据
select函数

#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

);

void FD_CLR(int fd, fd_set *set); 从set中清除fd
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设置为0(清零)

功能
  • 监控多路IO,看是否有就绪的
参数
  • nfds:要监控的文件描述符 最大值+1(也就是下面三个的最大值+1)
  • readfds:看是否可读的集合(最常用)
  • writefds:看是否可写的集合
  • exceptfds:看是否异常的集合
  • timeout:0(时间为0表示select是一个非阻塞调用)/ >0(时间大于0表示select阻塞对应的一段时间)/NULL(表示默认使用阻塞方式)
返回值
  • 成功返回 就绪的文件描述符的数量
  • 失败返回 -1而且错误码被设置
select使用流程

(1)准备监控的文件描述符表

fd_set readfds;

FD_ZERO(&readfds);

(2)添加要监控的文件描述符

FD_SET(fd,&readfds);

FD_SET(0,&readfds);

(3)准备nfds

nfds = fd+1;

(4)调用select函数

select(nfds,&readfds,NULL,NULL,NULL);

注意

(1)select函数调用完之后会将调用监控结果的文件描述符的集合带回给readfds,select监控到就绪的文件描述符后,会将未就绪的清零。

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
int fd;
void do_handler(int signo)
{
	char buf[1024]={0};
	int ret = read(fd,buf,sizeof(buf));
	perror("read test");
	printf("ret :%d buf :%s\n",ret,buf);
	if(strncmp(buf,"quit",4)==0)
	{
		exit(0);
	}

}
int main(int argc, const char *argv[])
{

	if (argc != 2)
	{
		printf("Usage: %s <fifo file>\n",argv[0]);
		return -1;
	}

	if (mkfifo(argv[1],0666) < 0 && errno!= EEXIST)
	{
		perror("mkfifo fail");
		return -1;
	}

	printf("success\n");

	//1.设置信号处理函数
	//signal(SIGIO,do_handler);

	//1.打开文件 
    //int fd = open(argv[1],O_RDONLY);
	int fd = open(argv[1],O_RDONLY|O_NONBLOCK);
	if (fd < 0)
	{
		perror("open fail");
		return -1;
	}

	//设置fd所属者
	//fcntl(fd,F_SETOWN,getpid());

#if 0
	//将fd设置为非阻塞
	int flag = fcntl(fd,F_GETFL,0);
	flag = flag | O_NONBLOCK;
	fcntl(fd,F_SETFL,flag);
#endif
	printf("read -------\n");

	//select使用流程
	//1.准备监控文件描述符
	fd_set readfds;
	FD_ZERO(&readfds);

	//2.添加要监控的文件描述符
	FD_SET(fd,&readfds);
	FD_SET(0,&readfds);

	//3.准备nfds
	int nfds = fd + 1;
	fd_set backfds;

	while (1)
	{
		backfds = readfds;
		//4.调用select函数
		int ret = select(nfds,&backfds,NULL,NULL,NULL);
		if(ret < 0)
		{
			perror("select fail");
			return -1;
		}
		printf("ret :%d\n",ret);
		if(ret > 0)
		{
			int i=0;
			for(i=0;i<nfds;i++)
			{
				if(FD_ISSET(i,&backfds))
				{
					if(i== fd)
					{
						//2.写
						char buf[1024] = {0};
						int ret = read(fd,buf,sizeof(buf));
						printf("ret :%d buf = %s\n",ret,buf);

						if (strncmp(buf,"quit",4) == 0)
						{
							break;
						}
					}else if(i == 0)
					{

						if(FD_ISSET(i,&backfds))
						{
							char buf[1024]={0};
							int ret = read(0,buf,sizeof(buf));
							printf("stdin ret:%d buf:%s\n",ret,buf);
							if(strncmp(buf,"quit",4)==0)
							{
								break;
							}
						}
					}
				}
			}
		}
	}
	//3.关闭 
	close(fd);
	if (unlink(argv[1]) < 0)
	{
		perror("unlink fail");
		return -1;
	}
	return 0;
}

(2)select监控超时,select函数每次会将timeout中的时间往下减,减法的结果放到timeout中,使用时,timeout中的值每次调用前,需要重新给值

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
int fd;
void do_handler(int signo)
{
	char buf[1024]={0};
	int ret = read(fd,buf,sizeof(buf));
	perror("read test");
	printf("ret :%d buf :%s\n",ret,buf);
	if(strncmp(buf,"quit",4)==0)
	{
		exit(0);
	}

}
int main(int argc, const char *argv[])
{

	if (argc != 2)
	{
		printf("Usage: %s <fifo file>\n",argv[0]);
		return -1;
	}

	if (mkfifo(argv[1],0666) < 0 && errno!= EEXIST)
	{
		perror("mkfifo fail");
		return -1;
	}

	printf("success\n");

	//1.设置信号处理函数
	//signal(SIGIO,do_handler);

	//1.打开文件 
    //int fd = open(argv[1],O_RDONLY);
	int fd = open(argv[1],O_RDONLY|O_NONBLOCK);
	if (fd < 0)
	{
		perror("open fail");
		return -1;
	}

	//设置fd所属者
	//fcntl(fd,F_SETOWN,getpid());

#if 0
	//将fd设置为非阻塞
	int flag = fcntl(fd,F_GETFL,0);
	flag = flag | O_NONBLOCK;
	fcntl(fd,F_SETFL,flag);
#endif
	printf("read -------\n");

	//select使用流程
	//1.准备监控文件描述符
	fd_set readfds;
	FD_ZERO(&readfds);

	//2.添加要监控的文件描述符
	FD_SET(fd,&readfds);
	FD_SET(0,&readfds);

	//3.准备nfds
	int nfds = fd + 1;
	fd_set backfds;

	while (1)
	{
		//注意:每次调用前需要重新给值
		struct timeval tv = {3,0};
		backfds = readfds;

		//4.调用select函数
		int ret = select(nfds,&backfds,NULL,NULL,NULL);
		if(ret < 0)
		{
			perror("select fail");
			return -1;
		}
		printf("ret :%d\n",ret);
		if(ret > 0)
		{
			int i=0;
			for(i=0;i<nfds;i++)
			{
				if(FD_ISSET(i,&backfds))
				{
					if(i== fd)
					{
						//2.写
						char buf[1024] = {0};
						int ret = read(fd,buf,sizeof(buf));
						printf("ret :%d buf = %s\n",ret,buf);

						if (strncmp(buf,"quit",4) == 0)
						{
							break;
						}
					}else if(i == 0)
					{

						if(FD_ISSET(i,&backfds))
						{
							char buf[1024]={0};
							int ret = read(0,buf,sizeof(buf));
							printf("stdin ret:%d buf:%s\n",ret,buf);
							if(strncmp(buf,"quit",4)==0)
							{
								break;
							}
						}
					}
				}
			}
		}
	}
	//3.关闭 
	close(fd);
	if (unlink(argv[1]) < 0)
	{
		perror("unlink fail");
		return -1;
	}
	return 0;
}
select存在的问题
select实现服务器并发

1.listenfd=socket

2.bind

3.listen

//1.准备表

fd_set readfds;

//清空

FD_ZERO(&readfds);

//2.添加要监控的文件描述符到表中

//listenfd

FD_SET(listenfd,readfds);

//3.调用select函数进行监控

//1.nfds --> 最大的文件描述符+1

int nfds = listenfd + 1; //注意: 是三个集合中最大的文件描述符加1

int ret = 0;

int i = 0;

fd_set bakfds = readfds;

while (1)

{

bakfds = readfds;

ret = select(nfds,&bakfds,NULL,NULL,NULL);

if (ret > 0)

{ //表示有就绪的文件描述符

//寻找具体是哪个文件描述符就绪

for (i = 0; i < nfds; i++)

{

if (FD_ISSET(i,&bakfds))

{ //只能判断出 i 是否在bakfds中

if (i == listenfd) //意味着有客户端发起连接请求

{

connfd = accpet();

//实现并发,添加对新连接的套接字的监控

//1. 添加到要监控的表中,进行监控

FD_SET(connfd,&readfds);

//2.更新maxfd

if (connfd + 1> nfds)

nfds = connfd+1;

}

else //创建 --- 子进程 或 线程

{ //此时肯定是某个客户端对应的connfd的值

read(i,buf,sizeof(buf)); //从i中读值,

//因为i表示的是当前就绪的文件描述符

printf("buf == %s\n",buf);

}

}

}

}

}

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>

int server_init(const char *ip,unsigned short port)
{
	//1. socket
	int fd = socket(AF_INET,SOCK_STREAM,0);
	if (fd < 0)
	{
		perror("socket fail");
		return -1; 
	}
	//2. bind  
	struct sockaddr_in addr; //结构体 
	bzero(&addr,sizeof(addr));
    addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	addr.sin_port = htons(50000);
	if (bind(fd,(const struct sockaddr*)&addr,sizeof(addr))  < 0)
	{
		perror("bind fail");
		return -1;
	}
	//3.listen
	if (listen(fd,5) < 0)
	{
		perror("listen fail");
		return -1;
	}

	return fd;
}

//tcp 客户端 
int main(int argc, const char *argv[])
{
	
	//1.链接 
	int listenfd = server_init("127.0.0.1",50000);
	if (listenfd < 0)
	{
		printf("server_init fail\n");
		return -1;
	}

	//2.通信 
	struct sockaddr_in cliaddr;
	bzero(&cliaddr,sizeof(cliaddr));
	socklen_t len = sizeof(cliaddr);
	
	//1.准备fd_set 
	fd_set readfds;
	FD_ZERO(&readfds);

	//2.添加要监控的文件描述符
	FD_SET(listenfd,&readfds);
	
	//3.准备nfds
	int nfds = listenfd + 1;

	fd_set backfds; 
	while (1)
	{
		backfds = readfds;
		int ret = select(nfds,&backfds,NULL,NULL,NULL);
		if (ret < 0)
		{
			perror("select fail");
			return -1;
		}

		int i = 0;
		for (i = 0; i < nfds; ++i)
		{
			if (FD_ISSET(i,&backfds))
			{
				if (i == listenfd)
				{
					//4.accept 
					int connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&len);
					if (connfd < 0)
					{
						perror("accept fail");
						return -1;
					}
					printf("connfd = %d\n",connfd);
					printf("--------------------\n");
					printf("client ip = %s\n",inet_ntoa(cliaddr.sin_addr));
					printf("client port = %d\n",ntohs(cliaddr.sin_port));

					//添加connfd 到 readfds 
					FD_SET(connfd,&readfds);
					if (nfds < connfd+1)
					{
						nfds = connfd + 1;
					}
				}else 
				{
					char buf[1024];
					read(i,buf,sizeof(buf));
					printf("ser buf = %s\n",buf);

					if(strncmp(buf,"quit",4)==0)
					{
						//客户端退出时,关闭i和从readfds清除
						close(i);
						FD_CLR(i,&readfds);
					}
					char sbuf[1024];
					sprintf(sbuf,"server + %s\n",buf);
					write(i,sbuf,strlen(sbuf) + 1);
				}
			}
		}
	}
	return 0;
}
epoll函数

1、创建监控的epoll对象

epoll_create

int epoll_create(int size);

功能
  • 创建epoll对象,返回文件描述符
参数
返回值
  • 成功返回 epoll描述符
  • 失败返回 -1表示创建失败
epoll_ctl函数
功能
  • epoll对象的控制函数
参数
返回值
  • 成功返回 0表示控制成功
  • 失败返回 -1表示失败
epoll_wait
功能
  • 监控文件描述符是否就绪
参数
  • epfd:表示监控的epoll对象
  • events:表示获得就绪事件的一个结构体指针(理解为一个struct epoll_event类型的数组名)
  • maxevents:表示该数组的最大容量
  • timeout:表示超时的时间,单位:毫秒,如果是0表示非阻塞(等待0ms),如果是-1表示阻塞,我们也可以给一个非负数表示阻塞多少微秒
返回值
  • 成功返回 就绪的文件描述符的数量,如果设置了超时,超时时间内没有就绪的,则返回0
  • 失败返回 -1而且错误码会被设置
相关推荐
Johnstons2 小时前
AnaTraf 网络流量分析免费版:给运维多一双“眼睛”
运维·服务器·网络
峥嵘life2 小时前
Android16 EDLA更新25-12补丁导致【CTS】CtsWindowManagerDeviceAnimations存在fail项
android·linux·学习
草莓熊Lotso2 小时前
手搓简易 Linux 进程池:从 0 到 1 实现基于管道的任务分发系统
linux·运维·服务器·数据库·c++·人工智能
YMWM_3 小时前
linux文件快速传windows
linux·运维·服务器
星竹晨L3 小时前
Linux开发工具入门(一):开发三板斧(包管理器,vim,gcc/g++) 以及入门理解动静态库
linux·运维·服务器
sunxunyong10 小时前
CGroup配置
linux·运维·服务器
hy____12310 小时前
Linux_网络编程套接字
linux·运维·网络
若风的雨11 小时前
【deepseek】 Linux 调度延时分析
linux