Linux系统编程—多进程服务器

多进程服务器

采用多进程实现并发服务器是一种常见的有效方法。

当连接建立后,服务器调用fork函数产生新的子进程,子进程处理客户请求,同时父进程关闭连接套接字,然后等待另一客户的连接。子进程首先关闭监听套接字,然后处理客户请求,最后关闭连接套接字并退出进程。

产生新的子进程后,父进程要关闭连接套接字,而子进程关闭监听套接字。
这么做基于两点考虑:
1、由于父进程只负责接收客户请求,而子进程只负责处理客户请求,关闭不需要的套接字可节省系统资源。并且,父进程和子进程共享这么套接字,如果它们都对这些套接字进行操作,会产生不可预计的后果。
2、另一个原因,也是最重要的,是为了正确地关闭连接,和文件描述符一样,每个套接字描述符都有一个“引用计数”,“引用计数”,是指同时打开一描述符的次数。

例如一个文件被同时打开两次,则“引用计数”为2,根据以上模板,当socket函数返回后,listenfd的“引用计数”变为2,而系统只有在某描述符的“引用计数”为0时,才真正关闭该描述符,对于连接套接字而言,系统此时才真正关闭连接。当父进程关闭connfd时,connfd的“引用计数”减1,当子进程关闭connfd时,connfd的“引用计数”减为0,此时系统才会关闭该连接。

当服务器调用accept函数,阻塞并等待客户连接时,其状态如下


image.png

当accept函数返回后,服务器接收了客户连接并产生新的连接套接字connfd,此时状态如下。


image.png

然后服务器调用fork函数,产生新的子进程,此时,父进程和子进程共享listenfd和connfd,其状态如下。


image.png

最后,父进程关闭连接套接字,子进程关闭监听套接字,状态如上,此时子进程可通过该连接处理客户请求,而父进程继续监听新的客户请求。


image.png

多进程并发服务器实例

采用多进程并发服务器算法来实现,通过实例理解多进程并发服务器如何处理多客户,以及其性能情况。
该实例包括服务器和客户端程序。完成的功能与重复性服务器类似,具体功能如下:
1) 服务器等候客户连接请求,一旦连接成功则显示客户的地址,接着接收该客户的名字并显示。然后接收来自该客户的信息(字符串)。每当收到一个字符串,则显示该字符串,并将字符串反转,再将反转的字符发回客户端,之后继续等待接收该客户的信息直到该客户关闭连接。服务器有同时处理多个客户的能力。
2) 客户端首先与相应服务器连接。接着接收用户输入的客户名字,再将名字发送给服务器。然后接收用户输入的字符串,再将字符串发送给服务器,接收服务器发回的信息并显示。之后,继续等待用户输入直至用户输入Ctrl+D,客户关闭连接并退出。

服务器代码:

#include <stdio.h>           
#include <strings.h>          
#include <unistd.h>         
#include <sys/types.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <arpa/inet.h>
#define PORT 1234    
#define BACKLOG 2    
#define MAXDATASIZE 1000  

void process_cli(int connectfd, struct sockaddr_in client);

main() 
{ 
    int sockfd, connectfd;  
    pid_t pid;
    struct sockaddr_in server;  
    struct sockaddr_in client; 
  
    
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Creating socket failed.");
        exit(1);
    }
    
    int opt = SO_REUSEADDR;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    bzero(&server,sizeof(server));
    server.sin_family=AF_INET; 
    server.sin_port=htons(PORT); 
    server.sin_addr.s_addr = inet_addr("127.0.0.1"); 
    
    if (bind(sockfd, (struct sockaddr *)&server, sizeof(struct sockaddr)) == -1) { 
        perror("Bind error.");
        exit(1); 
    }    
    if(listen(sockfd,BACKLOG) == -1){  
        perror("listen() error\n"); 
        exit(1); 
    } 
    
    int sin_size=sizeof(struct sockaddr_in); 
    
    while(1)
    {
        if ((connectfd = accept(sockfd,(struct sockaddr *)&client,&sin_size))==-1) {
            perror("accept() error\n"); 
            exit(1); 
        } 
        
        if ((pid=fork())>0) {
            /* 父进程 */
            close(connectfd);
            continue;
        }
        else if (pid==0) {
            /*子进程*/
            close(sockfd);
            process_cli(connectfd, client);
            exit(0);     
        }
        else {
            printf("fork error\n");
            exit(0);
        }
    }
    close(sockfd);   /* 关闭监听的套接字 */         
} 
void process_cli(int connectfd, struct sockaddr_in client)
{
    int num ,i;
    char recvbuf[MAXDATASIZE], sendbuf[MAXDATASIZE], cli_name[MAXDATASIZE];
    printf("You got a connection from %s.  ",inet_ntoa(client.sin_addr) ); /* 打印客户端ip地址 */ 
    
    /*获取客户端名字信息 */
    num = recv(connectfd, cli_name, MAXDATASIZE,0);
    if (num == 0) {
        close(connectfd);
        printf("Client disconnected.\n");
        return;
    }
    cli_name[num - 1] = '\0';
    printf("Client's name is %s.\n",cli_name);
    
    while (num = recv(connectfd, recvbuf, MAXDATASIZE,0)) {
        recvbuf[num] = '\0';
        printf("Received client( %s ) message: %s",cli_name, recvbuf);
        for (i = 0; i < num - 1; i++) {
            sendbuf[i] = recvbuf[num - i -2];
        }
        sendbuf[num - 1] = '\0';
        send(connectfd,sendbuf,strlen(sendbuf),0);  
    }
    close(connectfd); /*关闭连接文件描述符*/ 
}

客户端代码:

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

#define PORT 1234   
#define MAXDATASIZE 100  

void process(FILE *fp, int sockfd);
char* getMessage(char* sendline,int len, FILE* fp);

int main(int argc, char *argv[]) 
{ 
    int fd;       
    struct sockaddr_in server; 
    if (argc !=2) {       
        printf("Usage: %s <IP Address>\n",argv[0]); 
        exit(1); 
    } 
    
    if ((fd=socket(AF_INET, SOCK_STREAM, 0))==-1){  
        printf("socket() error\n"); 
        exit(1); 
    } 
    bzero(&server,sizeof(server));
    server.sin_family = AF_INET; 
    server.sin_port = htons(PORT); 
    server.sin_addr = inet_addr("127.0.0.1"); 
    
    if(connect(fd, (struct sockaddr *)&server,sizeof(struct sockaddr))==-1){ 
        printf("connect() error\n"); 
        exit(1); 
    } 
    process(stdin,fd);
    close(fd);   
} 
void process(FILE *fp, int sockfd)
{
    char sendline[MAXDATASIZE], recvline[MAXDATASIZE];
    int numbytes;
    printf("Connected to server. \n");
    
    printf("Input name : ");
    if ( fgets(sendline, MAXDATASIZE, fp) == NULL) {
        printf("\nExit.\n");
        return;
    }
    send(sockfd, sendline, strlen(sendline),0);
     
    while (getMessage(sendline, MAXDATASIZE, fp) != NULL) {
        send(sockfd, sendline, strlen(sendline),0);
        if ((numbytes = recv(sockfd, recvline, MAXDATASIZE,0)) == 0) {
            printf("Server terminated.\n");
            return;
        }
        recvline[numbytes]='\0'; 
        printf("Server Message: %s\n",recvline); 
    }
    printf("\nExit.\n");
} 
char* getMessage(char* sendline,int len, FILE* fp) 
{
    printf("Input string to server:");
    return(fgets(sendline, MAXDATASIZE, fp));
}

执行结果:
服务器:


image.png

客户端1:


image.png

客户端2:
image.png

这里关于多进程服务端往往容易出现的问题——僵死进程

僵死进程(僵尸进程)

进程已运行结束,但进程的占用的资源未被回收,这样的进程称为僵死进程

在多进程并发服务器中需要额外注意。由于服务器长期运行并服务大量客户,而且每个客户都会产生多次连接,如果不处理僵死进程或处理不当,必将耗尽系统资源而导致系统崩溃。在多进程并发服务器实例中,我们首先启动服务器程序,然后同时运行客户1和2。

结果感觉很正确,但用命令ps -el查看过程的运行情况,会发现由于服务器未进行子进程的终止处理而导致了僵死进程的产生。


image.png

为了处理僵死进程,父进程必须调用wait或者waitid函数,对对进程并发服务器进行修改,由于当一个进程终止时,父进程将收到SIGCHLD信号,因此我们增加一个信号处理器,每当捕获到该信号时,信号处理便调用waitid函数

waitpid函数

等待制定进程的子进程返回,并修改状态。
1、原型:

       #include <sys/types.h>
       #include <sys/wait.h>
       pid_t waitpid(pid_t pid, int *status, int options);

2、描述:
当pid等于-1,option等于0时,该函数等同于wait()函数。否则该函数的行为由参数pid和options决定。

pid制定了父进程需要知道哪些子进程的状态,其中:

= -1 要求知道任何一个子进程的返回状态
> 0 要求知道进程号为pid的子进程的状态
< -1 要求知道进程组号为pid的绝对值的子进程状态

option参数为位方式表示的标志,它是各个标志以“或”运算组成的位图,每一个标志以字节中某个位置1表示:
1)WUNTRACED 报告任何未知但已停止运行的指定进程的子进程状态,该子进程的状态自停止运行时起就没有被报告过。
2)WCONTINUED 报告任何继续运行的指定进程的子进程状态,该子进程的状态自继续运行起就没有被报告过。
3)WNOHANG 若调用本函数时,指定进程的子进程状态,目前并不是立即有效的(即可被立即读取的),调用进程不被挂起执行。
4)WNOWAIT 保持那些将其状态设置在stat_loc()函数的进程处于可等待状态。该进程将等待直到下一次被要求其返回状态值。

3、返回值:
当一个子进程返回时,返回值为该子进程号,否则返回值为-1,同stat_loc()函数返回子进程的返回值。

信号处理僵死进程的核心代码

/*主函数中需要注册信号*/
struct sigaction act,oact;
act.sa_handler=sig_handler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
if(sigaction(SIGCHLD,&act,&oact)<0){
    perror("sigaction failed");
    exit(1);    
}

if ((connectfd = accept(sockfd,(struct sockaddr *)&client,&sin_size))==-1) {
      /*表示中断,处理,这里需要识别客户端的断开连接,方便为后续处理僵死进程*/
      if(errno==EINTR) continue; 
      perror("accept error");
      exit(1);
}

/*信号的处理函数*/
void sig_handler(int s){
    pid_t pid;
    int stat;
    while((pid=waitpid(-1,&stat,WNOHANG))>0){
        printf("child %d terminated\n,pid");
        }
    return;
}

完整代码:

#include <stdio.h>           
#include <strings.h>          
#include <unistd.h>         
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>

#define PORT 1234    
#define BACKLOG 2   
#define MAXDATASIZE 1000  

void process_cli(int connectfd, sockaddr_in client);
void sig_handler(int s);

main()
{
    int listenfd, connectfd;  
    pid_t pid;
    struct sockaddr_in server;
    struct sockaddr_in client;  
    
    struct sigaction    act, oact;
    act.sa_handler = sig_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if (sigaction(SIGCHLD, &act, &oact) < 0) {
        perror("Sigaction failed!");
        exit(1);
    }
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("Creating socket failed.");
        exit(1);
    }
    
    int opt = SO_REUSEADDR;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    bzero(&server,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(PORT);
    server.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (bind(listenfd, (struct sockaddr *)&server, sizeof(struct sockaddr)) == -1) {
        perror("Bind error.");
        exit(1);
    }    
    
    if(listen(listenfd,BACKLOG) == -1){  
        perror("listen() error\n");
        exit(1);
    }
    
   int sin_size=sizeof(struct sockaddr_in);
    while(1)
    {
        if ((connectfd = accept(listenfd,(struct sockaddr *)
                                &client,&sin_size))==-1) {
            if (errno == EINTR) continue;
            perror("accept() error\n");
            exit(1);
        }
        if ((pid=fork())>0) {
            close(connectfd);
            continue;
        }
        else if (pid==0) {
            close(listenfd);
            process_cli(connectfd, client);
            exit(0);     
        }
        else {
            printf("fork error\n");
            exit(0);
        }
    }
    close(listenfd);            
}

void process_cli(int connectfd, sockaddr_in client)
{
    int num;
    char recvbuf[MAXDATASIZE], sendbuf[MAXDATASIZE], cli_name
            [MAXDATASIZE];
    
    printf("You got a connection from %s.  ",inet_ntoa(client.sin_addr)
           );
    
    num = recv(connectfd, cli_name, MAXDATASIZE,0);
    if (num == 0) {
        close(connectfd);
        printf("Client disconnected.\n");
        return;
    }
    cli_name[num - 1] = '\0';
    printf("Client's name is %s.\n",cli_name);
    
    while (num = recv(connectfd, recvbuf, MAXDATASIZE,0)) {
        recvbuf[num] = '\0';
        printf("Received client( %s ) message: %s",cli_name, recvbuf);
        for (int i = 0; i < num - 1; i++) {
            sendbuf[i] = recvbuf[num - i -2];
        }
        sendbuf[num - 1] = '\0';
        send(connectfd,sendbuf,strlen(sendbuf),0);
    }
    close(connectfd);
}

void sig_handler(int s)
{
    pid_t pid;
    int stat;
    
    while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0){
        printf("child %d terminated\n", pid);
    }
    return;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,492评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,048评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,927评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,293评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,309评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,024评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,638评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,546评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,073评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,188评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,321评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,998评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,678评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,186评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,303评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,663评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,330评论 2 358

推荐阅读更多精彩内容