daemon 进程为什么要fork两次

本文深入解析了Linux下Daemon进程的工作原理及实现方式,强调其核心功能是避免终端产生的信号导致进程意外退出。
  daemon进程是后台守护进程,有时候也叫精灵进程(agent).linux 下server都是daemon进程。相信大部分开发人员都知道如何去写一个daemon进程。但是另一方面,大部分人不知道为什么要这么做,不少人是从某个地方copy一个函数,拿来主义。但是具体为什么这么实现,却不是很透彻。

  见过一些面试官或被面试人。很多人解释daemon进程存在的理由是因为僵死进程。或者输入输出。其实和这些东西一毛钱关系都没有。daemon函数存在的原因是因为控制终端由于某些原因(如断开终端链接)会发送一些信号的原因。而接收进城处理这些信号缺省动作会让进程退出。这些信号会由于终端上敲一些特殊按键而产生。

  贴一个daemon函数常见的实现:
  1. int daemon(void)
  2. {
  3.     pid_t pid = fork();

  4.     if( pid != 0 ) exit(0);//parent

  5.     //first children
  6.     if(setsid() == -1)
  7.     {
  8.        printf("setsid failed\n");
  9.        assert(0);
  10.        exit(-1);
  11.     }

  12.     umask(0);


  13.     pid = fork();

  14.     if( pid != 0) exit(0);
  15.   
  16.     //second children
  17.     chdir ("/");

  18.     for (int i = 0; i < 3; i++)
  19.     {
  20.         close (i);
  21.     }


  22.     int stdfd = open ("/dev/null", O_RDWR);
  23.     dup2(stdfd, STDOUT_FILENO);
  24.     dup2(stdfd, STDERR_FILENO);

  25.     return 0;
  26. }
    1 、第一次fork的作用是让shell 认为本条命令 已经终止,不用挂在终端输入上。还有一个作用是为后面setsid服务。setsid的调用者不能是进程组组长(group leader). 此时父进程是进程组组长。
    
    2 、setsid() 是本函数最重要的一个调用。它完成了daemon函数想要做的大部分事情。调用完整个函数。子进程是会话组长(sid==pid),也是进程组组长(pgid == pid),并且脱离了原来控制终端。到了这一步,基本上不管控制终端如何怎么样。新的进程都不会收到那些信号。

    3  、经过前面2个步骤,基本想要做的都做了。第2次fork不是必须的。也看到很多开源服务没有fork第二次。fork第二次主要目的是。防止进程再次打开一个控制终端。因为打开一个控制终端的前台条件是该进程必须是会话组长。再fork一次,子进程ID != sid(sid是进程父进程的sid)。所以也无法打开新的控制终端。

    daemon目的就是防止终端产生的一些信号让进程退出。上面函数并没有直接调用signal函数去处理它。而是间接通过fork和setsid函数使用更少代码优雅处理。而被有些人误以为是僵死进程的原因需要这样处理。

    当然,也有很多程序不是像上面函数那样去实现。而是直接通过忽略信号方式处理。这样其实也不错,因为这些信号很少会有用到的价值。直接忽略基本上不存在误杀的情况。反正达到最终目的就可以。条条大路通罗马。

   下面罗列一下控制终端会产生哪些信号。程序中只要处理好这些信号,同样能达到上面函数实现的目的。

   //后台进程读取/写入终端输入产生下面两个信号,或者控制终端不存在情况读取和写入会产生
   signal(SIGTTOU, SIG_IGN);
   signal(SIGTTIN, SIG_IGN);

   //按CTRL-C ,CTRL-\ CTRL-Z会向前台进程组发送下面这些信号
   signal(SIGINT,  SIG_IGN );
   signal(SIGQUIT, SIG_IGN );
   signal(SIGTSTP, SIG_IGN );
   
   //终端断开,会给会话组长或孤儿进程组所有成员发送下面信号
   signal(SIGHUP,  SIG_IGN );

   还有有些信号也可以由终端shell产生,需要关注
   signal(SIGCONT, SIG_IGN );
   signal(SIGSTOP, SIG_IGN );


  上面这些信号,应该有些程序缺省处理(SIG_DFL)本身动作就是忽略(SIG_IGN),不是退出进程。不过按照上面写也不会造成什么问题。
<think>我们正在讨论一个使用双重fork的多进程服务器程序。用户提供的代码片段中包含了两次fork两次close(sock)操作。我们需要解释双重fork的原理以及两次close(sock)的作用。首先,回顾代码片段:1.第一次fork:创建守护进程daemon)```cvoiddaemon_fork(){intpid=-1;pid=fork();if(pid<0){//错误处理}elseif(pid>0){//父进程退出exit(0);}else{//子进程(守护进程)继续运行return;}}```2.第二次fork:在处理客户端连接时创建子进程```cvoidsubprocess(intsock){intpid=-1;pid=fork();if(pid<0){//错误处理}elseif(pid>0){//父进程(这里可能是主进程或者守护进程)关闭客户端socket并返回close(sock);return;}else{//子进程:关闭监听socket,然后处理客户端通信close(listen_fd);//注意:这里代码片段中写的是close(listen_fd),但参数是sock,可能是笔误?实际上应该是关闭监听套接字processMsg(sock);exit(0);}}```另外,在代码中我们还看到,在创建守护进程后,主程序会继续运行(作为守护进程),然后设置监听等。当有客户端连接时,会调用`subprocess`函数来处理。现在,我们解释双重fork两次close(sock)的原因。###双重fork的目的1.**第一次fork(创建守护进程)**:目的是将程序变为守护进程daemon)。守护进程是在后台运行的进程,通常不受终端控制。第一次fork后,父进程退出,子进程继续运行(成为守护进程)。这样做的目的是让出终端控制,并且让子进程成为init进程的子进程(这样就不会有僵尸进程的问题,因为init会回收其子进程)。但是,这里只进行了一次fork,按照传统的守护进程创建步骤,通常还会进行setsid等操作,但代码中并没有体现,所以这里可能只是一个简单的后台运行。2.**第二次fork(处理客户端请求)**:当守护进程(主进程)接收到一个客户端连接时,它会调用`subprocess`函数,在该函数中再次fork。这次fork的目的是为每个客户端连接创建一个独立的子进程来处理通信。这样,服务器可以同时处理多个客户端连接。###两次close(sock)的作用在`subprocess`函数中,我们看到了两次关闭套接字:1.**在父进程中关闭`sock`**:```celseif(pid>0){close(sock);return;}```这里的父进程是处理客户端连接请求的进程(即守护进程)。在fork出子进程后,父进程不再需要这个客户端连接套接字(因为通信将由子进程处理),所以关闭它。这样可以释放父进程的资源,避免文件描述符泄漏。2.**在子进程中关闭`listen_fd`**:```celse{close(listen_fd);processMsg(sock);exit(0);}```子进程负责与客户端通信,它不需要监听套接字(`listen_fd`),所以关闭监听套接字。然后,子进程使用客户端套接字(`sock`)与客户端进行通信。通信结束后,子进程退出。###为什么需要双重fork?在传统的守护进程创建中,双重fork(即两次fork)是为了确保守护进程不会成为会话组长,从而避免重新获取终端控制权。但是,在这个代码片段中,第一次fork并没有调用setsid,所以实际上并没有完成完整的守护进程创建。因此,这里的第一次fork可能只是为了将进程放到后台运行(父进程退出,子进程继续)。而第二次fork是为了创建处理客户端请求的子进程。这是多进程服务器的典型做法:主进程负责监听,每当有一个新连接,就创建一个子进程来处理该连接。###双重fork的完整守护进程创建通常,创建守护进程的完整步骤包括:1.第一次fork:父进程退出,子进程继续。2.子进程调用setsid创建一个新会话,并成为该会话的组长。3.第二次fork:确保守护进程不是会话组长,防止它重新获取终端。4.改变工作目录到根目录。5.重设文件权限掩码。6.关闭不必要的文件描述符。但是,在用户提供的代码中,只有一次fork(在`daemon_fork`中),所以它并不是一个完整的守护进程创建。因此,这里的双重fork实际上是指:第一次fork(创建守护进程)和第二次fork(创建处理客户端请求的子进程)的组合。###总结-**两次fork**:-第一次fork:将主进程变为守护进程(后台运行)。-第二次fork:为每个客户端连接创建一个子进程来处理通信。-**两次close(sock)**:-在父进程(守护进程)中关闭客户端套接字:因为父进程不需要与客户端通信,关闭以释放资源。-在子进程中关闭监听套接字:因为子进程不需要监听新连接,关闭以释放资源。注意:在子进程中关闭监听套接字并不会影响父进程中的监听套接字,因为fork之后文件描述符被复制,但是关闭操作只影响当前进程的描述符。###引用说明根据引用[3]和[4],多进程服务器通过fork进程来处理每个客户端连接,避免主进程被阻塞,从而实现并发。引用[3]还提到了僵尸进程的问题,但在这个代码中,子进程退出后由init进程回收(因为父进程已经退出)或者通过其他方式回收(如信号处理),但代码片段中未体现回收子进程的部分,可能需要完善。引用[2]提到多线程和多进程的设计思路类似,主线程/进程负责监听,子线程/进程负责通信。[^1]:用户提供的代码片段[^2]:使用多进程和多线程实现服务器并发[^3]:TCP/IP网络编程(3)——多进程服务器实现[^4]:高并发服务器-使用多进程(Multi-Process)实现###相关问题根据用户的问题,我们生成了以下相关问题:1.在多进程服务器中,如何避免僵尸进程?2.除了多进程,还有哪些方法可以实现高并发服务器?3.为什么在子进程中要关闭监听套接字,在父进程中要关闭客户端套接字?4.如何设置守护进程的完整步骤?5.在多进程服务器中,如果父进程先于子进程退出,会发生什么?</think>### C语言双重fork实现多进程服务器的原理 在C语言多进程服务器中,双重`fork()`主要用于实现**守护进程化**和**客户端连接处理**两个关键目标。以下是核心原理和作用分析: #### 1. 第一次`fork()`:创建守护进程 ```c void daemon_fork() { pid = fork(); // 第一次fork if (pid > 0) exit(0); // 父进程退出 } ``` - **目的**:将程序转为后台守护进程(脱离终端控制) - **作用**: - 父进程退出后,子进程被init进程接管,成为独立服务 - 解除终端关联,避免程序被信号意外终止 - **关闭`sock`无关**:此阶段尚未涉及套接字操作 #### 2. 第二次`fork()`:处理客户端连接 ```c void subprocess(int sock) { pid = fork(); // 第二次fork if (pid > 0) { close(sock); // 父进程关闭客户端socket } else { close(listen_fd); // 子进程关闭监听socket processMsg(sock); // 处理通信 } } ``` #### 3. 两次`close(sock)`的作用 | 关闭位置 | 关闭对象 | 作用原理 | |------------------|----------------|-------------------------------------------------------------------------| | **父进程** | 客户端`sock` | 释放父进程的文件描述符,避免资源泄漏。子进程已继承副本,不影响通信[^3][^4] | | **子进程** | 监听`listen_fd` | 子进程无需监听新连接,关闭后可释放资源,专注处理当前客户端通信[^2][^4] | #### 4. 双重fork的整体流程 ```mermaid graph TD A[主进程] -->|fork1| B[父进程退出] A -->|fork1| C[守护进程] C -->|accept| D[新客户端连接] D -->|fork2| E[父进程关闭client_sock] D -->|fork2| F[子进程关闭listen_fd] F --> G[处理客户端请求] ``` #### 5. 关键设计思想 1. **资源隔离** 父子进程各自关闭不需要的socket: - 父进程保留`listen_fd`继续监听新连接 - 子进程保留`client_sock`处理专属客户端 2. **并发能力** 每个客户端连接由独立子进程处理,避免阻塞主监听循环[^4] 3. **僵尸进程预防** 通过`fork()`后父进程立即关闭子进程不需要的资源,配合信号处理或`waitpid()`避免僵尸进程[^3] > **注意**:代码片段中`close(listen_fd)`的参数名应为`sock`(客户端socket),实际需要关闭的是监听socket(通常命名为`listen_fd`)[^1] ### 相关问题 1. **在多进程服务器中,如何避免僵尸进程的产生?** 2. **为什么进程需要关闭监听套接字,而父进程要关闭客户端套接字?** 3. **除了多进程,还有哪些方法可以实现高并发服务器?** 4. **双重fork创建的守护进程与普通后台进程有何区别?** 5. **在多进程服务器中,如果父进程意外退出会对子进程造成什么影响?** [^1]: 原始代码片段中的套接字管理 [^2]: 多进程/多线程服务器设计原理 [^3]: 僵尸进程的产生与回收机制 [^4]: 高并发服务器资源隔离策略
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值