【IO与服务器】

本文详细介绍了Linux系统中的缓存IO及其缺点,主要关注IO模型,包括阻塞IO和非阻塞IO的工作原理。通过多进程并发TCP服务器和多线程并发TCP服务器的示例,阐述了阻塞IO在处理大规模服务请求时的局限性,以及非阻塞IO的轮询机制和CPU资源消耗问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、缓存 IO:

(1)什么时缓冲IO

缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。
在 Linux 的 缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
在这里插入图片描述

(2)缺点

(1)数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据

(2)拷贝操作所带来的 CPU 以及内存开销是非常大的。

网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。

刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段

第一阶段:等待数据准备 (Waiting for the data to be ready)。

第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to theprocess)。

对于socket流而言,

第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。

第二步:把数据从内核缓冲区复制到应用进程缓冲区。

(3)IO模型

网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者。
在这里插入图片描述

1.阻塞IO (blocking IO):最常用、最简单、效率最低

在这里插入图片描述
阻塞模式下用户进程需要等待两次,一次为等待io中的数据就绪,一次是等

待内核把数据拷贝到用户空间 。

实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用send()的同时,进程将被阻塞,在此期间,进程将无法执行任何运算或响应任何的网络请求。

一个简单的改进方案是在服务器端使用多线程(或多进程)

举例:创建三个有名管道,并向里面输入数据

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
	int fd1 = open("./f1", O_RDWR);
	int fd2 = open("./f2", O_RDWR);
	int fd3 = open("./f3", O_RDWR);

	char buf[64] = {0};

	int ret;
	while(1)
	{
		ret = read(fd1, buf, sizeof(buf));
		if(ret != -1)
		{
			printf("fd1=%s\n", buf);
		}
		memset(buf, 0, sizeof(buf));

	
		ret = read(fd2, buf, sizeof(buf));
		if(ret != -1)
		{
			printf("fd2=%s\n", buf);
		}
		memset(buf, 0, sizeof(buf));
	
		ret = read(fd3, buf, sizeof(buf));
		if(ret != -1)
		{
			printf("fd3=%s\n", buf);
		}
		memset(buf, 0, sizeof(buf));
	}

	close(fd1);
	close(fd2);
	close(fd3);

	return 0;
}

代码运行:
在这里插入图片描述
多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。

具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的CPU资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。

并发服务器模型 TCP

{

多进程并发服务器 tcpserver_fork.c

基本原理: 每连接一个客户端,创建一个子进程,子进程负责处理connfd(客户请求) 父进程处理sockfd(连接请求)。

细节处理:

回收子进程资源多线程并发服务器 tcpserver_pthread.c

基本原理: 每连接一个客户端,创建一个子线程,子线程负责处理connfd(客户请 求),主线程处理sockfd(连接请求)。

//注意:子线程结束时,需要进行资源回收

}

上述多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不 尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统 资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。

并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就 会造成性能上的瓶颈。

总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。

例:

<1>多进程并发tcp服务器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>

int tcpser_init(int port);
int recv_data(int connfd);
void signal_handler(int sig);

int main(int argc, char *argv[])
{ 
	signal(SIGCHLD,signal_handler);
	int sockfd=tcpser_init(2323);
	struct sockaddr_in clientaddr;
	
	int m=sizeof(clientaddr);
	printf("wait for the client\n");
	while(1)
	{
		//connfd 进行数据的收发
		int connfd=accept(sockfd,(struct sockaddr *)&clientaddr,&m);
		if(connfd<0)
		{
			perror("accept");
			exit(-1);
		}
		printf("client ip:%s client port:%d",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
		printf("%d is link\n",connfd);
		pid_t pid=fork();
		if(pid<0)
		{
			perror("fork");
			exit(-1);
		}
		if(pid==0)//子进程处理connfd
		{
			close(sockfd);
			recv_data(connfd);
			exit(0);
		}
		else
		{//父进程处理sockfd
			
			//回收子进程的资源(处理僵尸进程)
			continue;
		}
	
	}
		
	close(sockfd);
    	return 0;
} 

void signal_handler(int sig)
{
	waitpid(-1,NULL,WNOHANG);
}

int tcpser_init(int port)
{
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(sockfd<0)
	{
		perror("socket");
		exit(-1);
	}
	printf("sockfd=%d\n",sockfd);
	
	//端口复用函数:解决端口号被系统占用的情况
	int on=1;
	int n=setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
	if(n<0)
	{
		perror("setsockopt");
		exit(-1);
	}
	struct sockaddr_in seraddr;
	memset(&seraddr,0,sizeof(seraddr));
	seraddr.sin_family=AF_INET;
	seraddr.sin_port=htons(port);
	seraddr.sin_addr.s_addr=inet_addr("0");//自动路由,寻找IP地址
	
	int len=sizeof(seraddr);
	int ret=bind(sockfd,(struct sockaddr *)&seraddr,len);
	if(ret<0)
	{
		perror("bind");
		exit(-1);
	}
	ret=listen(sockfd,6);
	if(ret<0)
	{
		perror("listen");
		exit(-1);
	}
	return sockfd;
}


int recv_data(int connfd)//接收和发送数据
{
	char buf[256]={0};
	while(1)
	{
		int n=recv(connfd,buf,sizeof(buf),0);
		if(n<0)
		{
			perror("recv");
			exit(-1);
		}
		else if(n==0)//客户端关闭了
		{
			printf("%d is nulink\n",connfd);
			close(connfd);
			break;
		}
		printf("message:%s\n",buf);
		memset(buf,0,sizeof(buf));
	}
	return 0;
}

代码运行:
在这里插入图片描述

<2>多线程并发tcp服务器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h> 


int tcpser_init(int port);
int recv_data(int connfd);
void *fun(void *arg);

int main(int argc, char *argv[])
{ 
	signal(SIGCHLD,(void *)fun);
	int sockfd=tcpser_init(2323);
	struct sockaddr_in clientaddr;
	
	int m=sizeof(clientaddr);
	printf("wait for the client\n");
	while(1)
	{
		//connfd 进行数据的收发
		int connfd=accept(sockfd,(struct sockaddr *)&clientaddr,&m);
		if(connfd<0)
		{
			perror("accept");
			exit(-1);
		}
		printf("client ip:%s client port:%d",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
		printf("%d is link\n",connfd);
		
		pthread_t pid;
		pthread_create(&pid,NULL,fun,(void *)&connfd);
		
	}
	close(sockfd);
    	return 0;
} 

void *fun(void *arg)
{
	pthread_detach(pthread_self());
	int connfd=*(int *)arg;
	recv_data(connfd);
}

int tcpser_init(int port)
{
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(sockfd<0)
	{
		perror("socket");
		exit(-1);
	}
	printf("sockfd=%d\n",sockfd);
	
	//端口复用函数:解决端口号被系统占用的情况
	int on=1;
	int n=setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
	if(n<0)
	{
		perror("setsockopt");
		exit(-1);
	}
	struct sockaddr_in seraddr;
	memset(&seraddr,0,sizeof(seraddr));
	seraddr.sin_family=AF_INET;
	seraddr.sin_port=htons(port);
	seraddr.sin_addr.s_addr=inet_addr("0");//自动路由,寻找IP地址
	
	int len=sizeof(seraddr);
	int ret=bind(sockfd,(struct sockaddr *)&seraddr,len);
	if(ret<0)
	{
		perror("bind");
		exit(-1);
	}
	ret=listen(sockfd,6);
	if(ret<0)
	{
		perror("listen");
		exit(-1);
	}
	return sockfd;
}


int recv_data(int connfd)//接收和发送数据
{
	
	char buf[256]={0};
	while(1)
	{
		int n=recv(connfd,buf,sizeof(buf),0);
		if(n<0)
		{
			perror("recv");
			exit(-1);
		}
		else if(n==0)//客户端关闭了
		{
			printf("%d is nulink\n",connfd);
			close(connfd);
			break;
		}
		printf("message:%s\n",buf);
		memset(buf,0,sizeof(buf));
	}
	return 0;
}

代码运行结果:
在这里插入图片描述

2、非阻塞IO

什么时轮询:
非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。

需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

所以事实上,在非阻塞IO模型中,用户进程需要不断的主动询问kernel数据好了没有,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
eg:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

void setnoblock(int fd)
{
	int flag = fcntl(fd,F_GETFL,0); //获得属性
	flag = flag | O_NONBLOCK;       //修改属性(添加非阻塞)
	fcntl(fd,F_SETFL,flag);         //设置属性
}

int main(int argc, const char *argv[])
{
	int fd1 = open("./f1", O_RDWR);
	int fd2 = open("./f2", O_RDWR);
	int fd3 = open("./f3", O_RDWR);

	setnoblock(fd1);
	setnoblock(fd2);
	setnoblock(fd3);

	char buf[64] = {0};

	int ret;
	while(1)
	{
		ret = read(fd1, buf, sizeof(buf));
		if(ret != -1)
		{
			printf("fd1=%s\n", buf);
		}
		memset(buf, 0, sizeof(buf));

	
		ret = read(fd2, buf, sizeof(buf));
		if(ret != -1)
		{
			printf("fd2=%s\n", buf);
		}
		memset(buf, 0, sizeof(buf));
	
		ret = read(fd3, buf, sizeof(buf));
		if(ret != -1)
		{
			printf("fd3=%s\n", buf);
		}
		memset(buf, 0, sizeof(buf));
	}

	close(fd1);
	close(fd2);
	close(fd3);

	return 0;
}

代码运行结果:
在这里插入图片描述
缺点:
整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值