linux高并发服务器

发布于:2025-07-03 ⋅ 阅读:(16) ⋅ 点赞:(0)

多进程并发服务器

使用多进程并发服务器时要考虑以下几点:

  1. 父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)

  2. 系统内创建进程个数(与内存大小相关)

  3. 进程创建过多是否降低整体服务性能(进程调度)

server

#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include "wrap.h"
void free_process(int sig)
{
	pid_t pid;
	while(1)
	{
		//如果设置了选项 WNOHANG,而调用中 
		//waitpid() 发现没有已退出的子进程可等待,则返回 0;
		pid = waitpid(-1,NULL,WNOHANG);
		if(pid <=0 )// pid<=0 表示无待回收子进程
		{
			break;
		}
		else
		{
			printf("child pid =%d\n",pid);
		}
	}



}
int main(int argc, char *argv[])
{
	sigset_t set;
	sigemptyset(&set);// //将set集合置空
	/* 1) 子进程终止时
	 * 2) 子进程接收到SIGSTOP信号停止时
	 * 3) 子进程处在停止态,接受到SIGCONT后唤醒时
	 */
	sigaddset(&set,SIGCHLD);//将SIGCHLD信号加入到set集合

	//信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞
	//(在信号发生时记住它,直到进程准备好时再将信号通知进程)防止fork()前子进程退出导致信号丢失
	sigprocmask(SIG_BLOCK,&set,NULL);
	//创建套接字,绑定
	int lfd = tcp4bind(8008,NULL);
	//监
	Listen(lfd,128);
	//提取
	//回射
	struct sockaddr_in cliaddr;
	socklen_t len = sizeof(cliaddr);
	while(1)
	{
		//进程分工​:
			//​子进程​:处理客户端请求(cfd)
			//​父进程​:继续监听新连接(lfd)

		//关键资源管理​:
			//​子进程关闭lfd​:避免文件描述符泄漏
			//​父进程关闭cfd​:减少引用计数,客户端断开时子进程可正常退出


		char ip[16]="";
		//提取连接,
		int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
		printf("\nnew client ip=%s port=%d\n",
                inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),ntohs(cliaddr.sin_port));
		//fork创建子进程
		pid_t pid;
		pid = fork();
		if(pid < 0)
		{
			perror("");
			exit(0);
		}
		else if(pid == 0)//子进程
		{
			// 关闭监听套接字(子进程不需要)
			close(lfd);
			while(1)
			{
			char buf[1024]="";

			int n = read(cfd,buf,sizeof(buf));
			if(n < 0)
			{
				perror("");
				close(cfd);
				exit(0);
			}
			else if(n == 0)//对方关闭j
			{
				printf("\nclient close\n");
				close(cfd);
				exit(0);
			
			}
			else
			{
				struct sockaddr_in remote_addr;
				socklen_t len = sizeof(remote_addr);
				getpeername(cfd, (struct sockaddr *)&remote_addr, &len);
				int remote_port = ntohs(remote_addr.sin_port);

				printf("客户端%d:%s",remote_port,buf);
				write(cfd,buf,n);
				// exit(0);	
			}
			}
		
		}
		else//父进程
		{
			// 关闭客户端套接字(父进程不需要)
			close(cfd);
			//回收
			//注册信号回调
			struct sigaction act;
			act.sa_flags =0;
			act.sa_handler = free_process;//free_process函数指针传给act结构体
			sigemptyset(&act.sa_mask);//把信号阻赛集清空
			sigaction(SIGCHLD,&act,NULL);//遇到SIGCHLD信号调用free_process函数
			sigprocmask(SIG_UNBLOCK,&set,NULL);//:从信号阻塞集合中删除 set 信号集
		/*整个代码意思就是在注册回调函数之前先把SIGCHLD信号阻塞,回调函数注册好了
 * 之后,在从set阻塞信号集中删除SIGCHLD信号,使得sigaction函数能够捕捉到SIGCHLD信号,从而调用free_process函数回收进程资源*/
		}
	}
	//关闭



	return 0;
}


在 Linux 系统中,当父进程通过 fork() 创建子进程时,​子进程会复制父进程的文件描述符表,包括 socket 描述符。这种复制并非创建新的 socket 资源,而是共享相同的底层文件表项​(即内核中的打开文件结构)。

文件描述符的复制机制

  • 共享相同内核对象​:
    子进程复制的文件描述符与父进程指向同一个内核文件表项​(包括 socket 结构)。
    例如:父进程的 socket 描述符 fd=3,子进程的 fd=3 也指向同一个 socket 对象。
  • 引用计数增加​:
    每复制一次描述符,内核中该文件表项的引用计数会​+1。只有所有进程关闭描述符后(引用计数归零),内核才会释放资源。

父子进程对描述符的操作独立性

  • 修改互不影响​:
    父子进程可独立操作复制的描述符(如 read/write/close),但操作会影响同一个 socket 资源:
    • 读写共享缓冲区​:一方发送数据,另一方会收到;一方关闭连接,另一方会感知(如 read 返回 0)。
    • 关闭操作独立​:子进程关闭 fd 不影响父进程的 fd,但会减少引用计数。
  • 典型多进程服务器模型​:
    • 父进程​:关闭客户端连接描述符​(close(cfd)),仅保留监听套接字。
    • 子进程​:关闭监听描述符​(close(lfd)),仅保留客户端连接描述符。

client

客户端使用:

nc 127.0.0.1 8008

模拟客户端链接服务器

结果显示如下:

多线程并发服务器

在使用线程模型开发服务器时需考虑以下问题:

  1. 调整进程内最大文件描述符上限

  2. 线程如有共享数据,考虑线程同步

  3. 服务于客户端线程退出时,退出处理。(退出值,分离态)

  4. 系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU

server

#include <stdio.h>
#include <pthread.h>
#include <string.h>  
#include <arpa/inet.h>  
#include <ctype.h>  
#include <unistd.h>  
#include <fcntl.h>  

#include "wrap.h"

// 作用​:封装客户端连接信息
// ​设计意图​:避免线程参数传递时出现竞争(主线程的accept循环与线程启动存在时间差)
// ​动态内存分配​:每个连接独立分配malloc,确保线程安全(避免共用栈变量)
typedef struct c_info
{
	int cfd;
	struct sockaddr_in cliaddr;

}CINFO;

void* client_fun(void *arg);
int main(int argc, char *argv[])
{
	if(argc < 2)
	{
		printf("argc < 2???   \n ./a.out 8000 \n");
		return 0;
	}

	//PTHREAD_CREATE_DETACHED:线程自动回收资源,​无需主线程调用​ pthread_join
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);

	short port = atoi(argv[1]);
	int lfd = tcp4bind(port,NULL);//创建套接字 绑定 
	Listen(lfd,128);
	struct sockaddr_in cliaddr;
	socklen_t len = sizeof(cliaddr);
	CINFO *info;
	while(1)
	{
		// Accept():阻塞等待新连接,返回客户端socket (cfd)
		// 动态创建CINFO对象存储客户端信息
		// pthread_create:启动线程处理连接,传入info指针
		int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
		char ip[16]="";
		pthread_t pthid;
		info = malloc(sizeof(CINFO));
		info->cfd = cfd;
		info->cliaddr= cliaddr;
		pthread_create(&pthid,&attr,client_fun,info);
	
	
	
	}

	return 0;
}

void* client_fun(void *arg)
{
	CINFO *info = (CINFO *)arg;
	char ip[16]="";

	printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&(info->cliaddr.sin_addr.s_addr),ip,16),ntohs(info->cliaddr.sin_port));
	while(1)
	{
		char buf[1024]="";
		int count=0;
		count = read(info->cfd,buf,sizeof(buf));
		if(count < 0)
		{
			perror("");
			break;
		
		}
		else if(count == 0)
		{
			printf("client close\n");
			break;
		}
		else
		{
			printf("%s\n", buf);
			write(info->cfd,buf,count);
		
		}
	
	
	}
	close(info->cfd);
	free(info);
}

client

客户端使用:

nc 127.0.0.1 8008

模拟客户端链接服务器

结果显示如下:

总结:

  • 优势​:
    • 高并发​:线程切换开销远小于进程(约1/10~1/100)1
    • 资源共享​:线程共享进程内存,数据传递高效(对比进程需IPC)
  • 风险​:
    • 线程数过多时,上下文切换开销增大(需线程池优化)
    • 共享资源需同步(本例无共享数据,故未用锁)

两者之间对比

1. 资源开销对比

  • 进程高并发

    • 创建/销毁开销大​:每个进程需独立分配内存、文件描述符等资源,系统调用(如 fork())耗时长。
    • 内存占用高​:多进程独立内存空间导致总内存消耗线性增长,例如10个进程可能占用10倍于单进程的内存。
    • 上下文切换慢​:切换进程需保存/恢复完整的地址空间、页表等,开销约为线程的 ​10~100倍​。
  • 线程高并发

    • 轻量级创建​:线程仅需分配栈和寄存器,共享进程资源(如内存句柄),创建速度比进程快 ​5~10倍​。
    • 内存共享​:多个线程共享同一进程的内存,内存占用远低于多进程(如100线程共享同一堆)。
    • 切换开销低​:仅需保存寄存器、栈指针等少量状态,切换速度更快。

2. 隔离性与稳定性

  • 进程高并发

    • 强隔离性​:进程崩溃不影响其他进程(如Apache的prefork模式)。
    • 安全性高​:独立地址空间避免内存越界污染(适合沙箱环境)。
  • 线程高并发

    • 弱隔离性​:单线程崩溃(如段错误)可能导致整个进程终止(所有线程退出)。
    • 共享资源风险​:线程间共享变量需严格同步,否则易引发数据竞争(如未加锁的全局计数器)。

3. 通信效率

  • 进程高并发

    • IPC机制复杂​:需管道、消息队列、共享内存等,数据需内核态拷贝(如 memcpy),延迟高。
    • 共享内存例外​:虽高效但需手动同步(如信号量),编程复杂度高。
  • 线程高并发

    • 直接共享内存​:通过全局变量即可通信(如生产者-消费者模型),速度比IPC快 ​10~100倍​。
    • 同步成本​:需锁(互斥锁、条件变量)避免竞态,不当使用易导致死锁

相比之下,这两种方式在高并发服务器场景中各具优势,因而催生了多路I/O转接服务器(select、poll、epoll)的设计理念。其核心思想是将I/O事件的轮询工作交给内核,应用程序仅在事件就绪时被唤醒处理,从而避免为每个连接创建独立线程/进程的开销。

下一篇文章将详细讲解select、poll和epoll三种I/O模型。