Linux Socket编程——多进程并发

1.引言

  如果你还不了解Linux下的Socket编程,可见花几分钟看看这篇文章,很详细,很经典!文章代码如有困难,可以联系博主~~
Linux Socket编程入门——浅显易懂


2.多进程编程

 2.1 进程的概述

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

  • 父进程最大文件描述符个数(父进程中需要close关闭、accept返回的新的文件描述符)
  • 系统内创建进程个数(与内存大小有关)
  • 进程创建过多是否降低整体服务性能(进程调度)

  一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

一个多进程实例(理解一下多进程)

#include <unistd.h>
#include <stdio.h> 
int main () 
{ 
   pid_t fpid; //fpid表示fork函数返回的值
   int count=0;
   fpid=fork();    //建立子进程
   if (fpid < 0) 
   	printf("error in fork!"); 
   else if (fpid == 0) {
   	printf("i am the child process, my process id is %d/n",getpid()); 
   	printf("我是爹的儿子/n");//对某些人来说中文看着更直白。
   	count++;
   }
   else {
   	printf("i am the parent process, my process id is %d/n",getpid()); 
   	printf("我是孩子他爹/n");
   	count++;
   }
   printf("统计结果是: %d/n",count);
   return 0;
}

运行结果是:

 i am the child process, my process id is 5574
 我是爹的儿子
 统计结果是: 1
 i am the parent process, my process id is 5573
 我是孩子他爹
 统计结果是: 1

  在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)……
  为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;

  在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID(>0)我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

 2.2 僵尸进程

  在linux系统中,当用ps命令观察进程的执行状态时,经常看到某些进程的状态栏为defunct,这就是所谓的“僵尸”进程。“僵尸”进程是一个早已死亡的进程,但在进程表(processs table)中仍占了一个位置(slot)。由于进程表的容量是有限的,所以,defunct进程不仅占用系统的内存资源,影响系统的性能,而且如果其数目太多,还会导致系统瘫痪。

  当子进程退出的时候,内核会向父进程发送SIGCHLD信号,父进程可以选择忽略这个信号。子进程的退出是个异步事件(子进程可以在父进程运行的任何时刻终止)。 子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态。

  如上可知,僵尸进程一旦出现之后,很难自己消亡,会一直存在下去,直至系统重启。虽然僵尸进程几乎不占系统资源,但是,这样下去,数量太多了之后,终究会给系统带来其他的影响。因此,如果一旦见到僵尸进程,我们就要将其杀掉。如何杀掉僵尸进程呢?

  • 方法一:杀掉僵尸进程的父进程,这样僵尸进程就变成孤儿进程,从而init进程将代替父进程来接手,负责清除这个孤儿进程。
  • 方法二:重启电脑

杀掉的方法其实效果并不好,好的办法是预防僵尸进程的产生

  • 通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核负责回收僵尸进程。但这样就无法获得进程退出状态。
  • 通过捕捉SIGCHLD信号,调用waitpid()函数来处理僵尸进程。

 2.3 捕捉信号、调用waitpid函数回收

信号的注册、捕捉

#include <signal.h>

int sigaction(int signum,   //捕捉的信号
			const struct sigaction *act,  //捕捉到信号后的行为
            struct sigaction *oldact);  //一般为NULL

struct sigaction {
               void     (*sa_handler)(int);  //处理函数
               void     (*sa_sigaction)(int, siginfo_t *, void *); //一般不会用
               sigset_t   sa_mask;   //在信号处理函数执行过程中,临时屏蔽掉指定信号。如果没有需要,清空该操作就行。
               int        sa_flags;  //默认为0,选择sa_handler指针
               //void     (*sa_restorer)(void);
           };


回收函数:waitpid()函数原型如下:

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

调用一次waitpid(),只能回收一个进程。通常情况下,用循环来回收。

参数

  • pid:
    pid == -1: 回收所有的子进程
    pid > 0 :某个子进程的pid
    pid == 0 :回收当前进程组的所有子进程
    pid < -1 :将不在同一进程组的指定pid进程进行回收,也就是等待进程组号为pid绝对值的任何子进程。
  • status:这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会退出,是正常推出还是出了什么错误。当然,如果不关心子进程为什么推出的话,也可以传入空指针。
  • options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起等待(设置非阻塞)。如果设置为0,就是阻塞。

返回值

  • -1:回收失败,没有子进程
  • >0:返回被回收的子进程的pid
  • 如果为非阻塞,
    =0:子进程处于运行状态

3. 代码实例

  PS:这里要注意一下accept()函数的返回值处理,因为如果一个子进程结束、或者其他信号的错误,会导致accept()函数出错。这里主要注意:ECONNABORTED、EINTR这两个返回错误信息。
在这里插入图片描述

  代码目标多个客户端去连接一个服务端,服务端将接收到的小写字母转换为大写字母,再传回对应的客户端。

/***
Server端

Author:Liang jie
objective:服务端将客户端输入的小写转换为大写,再传回客户端。
*/ 

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#define SRV_PORT 10005
#define BUFIZE 4096

 //进程的回收函数 
void catch_child(int signum){
	while(waitpid(0,NULL,WNOHANG)>0);   //非阻塞回收 
	return;
}


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

        int lfd,cfd;
        pid_t pid;
        int ret,i;
        char buf[BUFIZE];

        struct sockaddr_in srv_addr,clt_addr;
        socklen_t clt_addr_len;
        
        bzero(&srv_addr,sizeof(srv_addr));   //清空地址结构 
        
        srv_addr.sin_family=AF_INET;
        srv_addr.sin_port=htons(SRV_PORT);
        srv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
        
	lfd = socket(AF_INET,SOCK_STREAM,0);
        if(lfd<0)
    {
        printf("socket creation failed\n");
        exit(-1);
    }


        if(bind(lfd,(struct sockaddr *)&srv_addr,sizeof(srv_addr))==-1)   
    {
        printf("Bind error.\n");
        exit(-1);
    }

        
         if(listen(lfd,128)==-1)
    {
        printf("Listen error!\n");
        exit(-1);
    }
       printf("Start to listen!\n");
        
        
        clt_addr_len=sizeof(clt_addr);


while(1)
        {
        /*accept()函数,这里要说明一下,因为accept函数可能被信号量终止,
        也就是说,accept函数可能并没有出错,只是因为其他信号量出了问题。
        所以这里增加了对accept函数返回值的处理,以防被后面进程的信号量打断*/
 	again:
        if ((cfd=accept(lfd,(struct sockaddr *)&clt_addr,&clt_addr_len))<0)
		{
        	if((errno==ECONNABORTED) || (errno==EINTR))
        	  goto again;
        	else{	
        	     printf("accept error!\n");
       		     exit(-1);	
			    }
      	
		}
        
        //创建子进程 
        pid=fork();
        
        //如果创建失败 
        if(pid<0){
          printf("fork error");
          exit(-1);
        }
		//如果是子进程 
		else if(pid==0){
          close(lfd);
          break;
        }
        //如果是父进程,则需要回收已经结束的子进程
		else{   
 	 	struct sigaction act;
		act.sa_handler=catch_child;   //信号响应函数 
		sigemptyset(&act.sa_mask);   //清空该操作
		act.sa_flags=0;             //选择第一种函数指针 sa_handler

		ret=sigaction(SIGCHLD,&act,NULL);  //注册信号,需要捕捉的信号:SIGCHLD。act是捕捉到该信号后的反应 
	
		if (ret!=0){
		 printf("error");
		 exit(-1);
		}
        close(cfd);
        continue;
      }

        }
	//如果是子进程,(写在While循环里面显得太臃肿,所以就把子进程写在外边) 
        if (pid==0){
        for(;;){
	    ret = read(cfd,buf,sizeof(buf));
        if (ret==0){   //检测到客户端关闭 
        close(cfd);
        exit(1);
        }
        //小写转大写 
        for(i=0;i<ret;i++)
           buf[i]=toupper(buf[i]);
           
        write(cfd,buf,ret);   //写回客户端 
	    write(STDOUT_FILENO,buf,ret);   //写到屏幕上 
	    printf("\n");
	}
	}

 return 0;
}

Client端和上一篇文章一样,不用改动。
点击,这里是文章地址~~


4.总结

  文章从进程的概念开始讲解,到创建子进程,再到创建子进程经常会出现的僵尸进程、如何解决僵尸进程、以及accept()函数的返回值处理。
  下一篇文章将会讲解多线程下的socket编程。
  文章代码如有困难,可以联系博主~~

发布了36 篇原创文章 · 获赞 65 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43275558/article/details/105041490