Linux - 多进程、线程服务器实现

本文详细介绍了Linux中缓存IO的工作机制,指出了其在数据传输中的多次拷贝操作导致的性能开销。网络IO的本质是socket的读取,涉及等待数据准备和从内核拷贝到进程的两个阶段。针对阻塞IO的问题,文章提出了多线程作为解决方案,通过示例代码展示了如何在服务器端使用多线程来处理并发连接,避免单个连接阻塞其他连接的执行。

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

一、为什么需要?

1.缓存IO

缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的

缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,
也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区
拷贝到应用程序的地址空间
缓存 IO 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据
拷贝操作所带来的 CPU 以及内存开销是非常大的。
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操
作。
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区
中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read
操作发生时,它会经历两个阶段:
        
第一阶段:等待数据准备 (Waiting for the data to be ready)。
第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the
process)。
对于socket流而言:
        
第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
第二步:把数据从内核缓冲区复制到应用进程缓冲区。
网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延
迟,给应用带来的性能瓶颈大于后者。
图示:
        

2.在阻塞IO中

UNIX/Linux下主要有4I/O 模型:
阻塞I/O:
最常用、最简单、效率最低
非阻塞I/O:
 可防止进程阻塞在I/O操作上,需要轮询
I/O 多路复用:
 允许同时对多个I/O进行控制
信号驱动I/O:
 一种异步通信模型

阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的
I/O 。
缺省情况下,套接字建立后所处于的模式就是阻塞I/O 模式。
很多读写函数在调用过程中会发生阻塞。
读操作中的read、recv、recvfrom
写操作中的write、send
其他操作:accept、connect

3.阻塞概况

1)kernel(内核)就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在
一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的
数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一
个过程的。而在用户进程这边,整个进程会被阻塞。
2)第二个阶段:当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用
户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来
也就是说阻塞模式下用户进程需要等待两次,一次为等待io中的数据就绪,一次是等
待内核把数据拷贝到用户空间
实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给
网络编程带来了一个很大的问题,如在调用send()的同时,进程将被阻塞,在此期间,进程
将无法执行任何运算或响应任何的网络请求。

二、怎么做?

一个简单的改进方案是在服务器端使用多线程(或多进程)。(当然还有多种解决方式)
多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一
个连接的阻塞都不会影响其他的连接。
具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远
远大于线程,所以如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个
服务执行体需要消耗较多的CPU资源,譬如需要进行大规模或长时间的数据运算或文件访
问,则进程较为安全。

1.多进程服务器

代码实例

服务器端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>        
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <time.h>
#include <signal.h>            //头文件

int tcpserver_init(int port);  //创建套接字,绑定端口号和IP地址,设置监听
int recv_data(int connfd);     //数据收发函数
void signal_handler(int sig);      //用信号来处理子进程的资源回收,为什么用信号?1.用wait会阻塞2.用非阻塞,即使不阻塞也会一直询问,占用资源   用信号的好处是当子进程先结束时用signal函数自动接收,处理资源回收,不阻塞,资源也占用少;

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

	signal(SIGCHLD, signal_handler);//处理子进程资源回收

	int sockfd = tcpserver_init(6666);//6666代表端口号

	struct sockaddr_in clientaddr;
	
	int m = sizeof(clientaddr);

	printf("wait a client.............\n");

	while(1)
	{
		//connfd 进行数据的收发
		int connfd = accept(sockfd,  (struct sockaddr *)&clientaddr , &m );//设置等待
		if(connfd < 0)
		{
			perror("accept");
			return -1;
		}

		printf("client ip: %s client port: %d\n", 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); 
		}
		else if(pid == 0) //子进程处理connfd,接收数据
		{
			close(sockfd);
			
			recv_data(connfd);
		
			exit(0);
		}
		else{ //父进程处理sockfd
			
			//回收子进程的资源(处理僵尸进程)//single
			continue;
		}

	}

	close(sockfd);

	return 0;
}


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


int tcpserver_init(int port)
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0)
	{
		perror("sockfd");
		return -1;
	}

	printf("sockfd = %d\n", sockfd);

	//端口复用函数:解决端口号被系统占用的情况
	int on = 1;
	int k = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	if(k == -1)
	{
		perror("setsockopt");
		return -1;
	}

	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0 ,sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(port);
	serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址

	int len = sizeof(serveraddr);

	int ret = bind(sockfd, (struct sockaddr *)&serveraddr, len);
	if(ret == -1)
	{
		perror("bind");
		return -1;
	}


	ret = listen(sockfd, 5);
	if(ret == -1)
	{
		perror("listen");
		return -1;
	}

	return sockfd;
}

int recv_data(int connfd)
{
		//接收和发送数据
		char buf[1024] = {0};

		while(1)
		{
			int n = recv(connfd, buf, sizeof(buf), 0);	
			if(n < 0)
			{
				perror("read");
				return -1;
			}
			else if(n == 0) //客户端关闭了
			{
				printf("%d is unlink\n", connfd);
				close(connfd);
				break;	
			}

			printf("message:%s\n", buf);

			memset(buf, 0 , sizeof(buf));
		}

		return 0;
}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>         
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建客户端套接字
	if(sockfd < 0)
	{
		perror("sockfd");
		return -1;
	}

	printf("sockfd = %d\n", sockfd);


	struct sockaddr_in serveraddr,clientaddr;//设置连接参数
	memset(&serveraddr, 0 ,sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(6666);
	serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址

	int len = sizeof(serveraddr);

	int ret = connect(sockfd, (struct sockaddr *)&serveraddr, len);
	if(ret == -1)
	{
		perror("connect");
		return -1;
	}

	printf("connect success!\n");

	//接收和发送数据
	char buf[1024] = {0};

	while(1)
	{
		gets(buf); 

		send(sockfd, buf, strlen(buf), 0);	

		memset(buf, 0, sizeof(buf));
	}

	close(sockfd);

	return 0;
}

2.多线程服务器

服务器端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>

int tcpserver_init(int port);
int recv_data(int connfd);

void *fun(void *arg) //子线程
{
	pthread_detach( pthread_self() );//回收线程
	
	int connfd = (int)arg;
	
	recv_data(connfd);//接收数据
}

int main(int argc, const char *argv[])//主线程
{

	int sockfd = tcpserver_init(6666);

	struct sockaddr_in clientaddr;
	
	int m = sizeof(clientaddr);

	printf("wait a client.............\n");
	
	while(1)
	{
		//connfd 进行数据的收发
		int connfd = accept(sockfd,  (struct sockaddr *)&clientaddr , &m );
		if(connfd < 0)
		{
			perror("accept");
			return -1;
		}

		printf("client ip: %s client port: %d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

		printf("%d is link\n", connfd);

		pthread_t tid;//创建子线程
		pthread_create(&tid, NULL, fun , (void *)connfd);

	}

	close(sockfd);

	return 0;
}



int tcpserver_init(int port)
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0)
	{
		perror("sockfd");
		return -1;
	}

	printf("sockfd = %d\n", sockfd);

	//端口复用函数:解决端口号被系统占用的情况
	int on = 1;
	int k = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	if(k == -1)
	{
		perror("setsockopt");
		return -1;
	}

	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0 ,sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(port);
	serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址

	int len = sizeof(serveraddr);

	int ret = bind(sockfd, (struct sockaddr *)&serveraddr, len);
	if(ret == -1)
	{
		perror("bind");
		return -1;
	}


	ret = listen(sockfd, 5);
	if(ret == -1)
	{
		perror("listen");
		return -1;
	}

	return sockfd;
}

int recv_data(int connfd)
{
		//接收和发送数据
		char buf[1024] = {0};

		while(1)
		{
			int n = recv(connfd, buf, sizeof(buf), 0);	
			if(n < 0)
			{
				perror("read");
				return -1;
			}
			else if(n == 0) //客户端关闭了
			{
				printf("%d is unlink\n", connfd);
				close(connfd);
				break;	
			}

			printf("message:%s\n", buf);

			memset(buf, 0 , sizeof(buf));
		}

		return 0;
}
客户端代码:同进程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值