2005年九月月 发布的文章

解疑sigsuspend

Unix提供了等待信号的系统调用,sigsuspend就是其中一个,在CU(www.chinaunix.net)上曾经讨论过一个关于该系统调用的问题,这里也做一下解疑。

CU网友讨论的问题的核心就是到底sigsuspend先返回还是signal handler先返回。这个问题Stevens在《Unix环境高级编程》一书中是如是回答的“If a signal is caught and if the signal handler returns, then sigsuspend returns and the signal mask of the process is set to its value before the call to sigsuspend.”,由于sigsuspend是原子操作,所以这句给人的感觉就是先调用signal handler先返回,然后sigsuspend再返回。但其第一个例子这么讲又说不通,看下面的代码:
CU上讨论该问题起于中的该例子:
int main(void) {
   sigset_t   newmask, oldmask, zeromask;

   if (signal(SIGINT, sig_int) == SIG_ERR)
      err_sys("signal(SIGINT) error");

   sigemptyset(&zeromask);

   sigemptyset(&newmask);
   sigaddset(&newmask, SIGINT);
   /* block SIGINT and save current signal mask */
   if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
      err_sys("SIG_BLOCK error");

   /* critical region of code */
   pr_mask("in critical region: ");

   /* allow all signals and pause */
   if (sigsuspend(&zeromask) != -1)
      err_sys("sigsuspend error");
   pr_mask("after return from sigsuspend: ");

   /* reset signal mask which unblocks SIGINT */
   if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
      err_sys("SIG_SETMASK error");

   /* and continue processing … */
   exit(0);
}

static void sig_int(int signo) {
   pr_mask("\nin sig_int: ");
   return;
}
 
结果:
$a.out
in critical region: SIGINT
^C
in sig_int: SIGINT
after return from sigsuspend: SIGINT

如果按照sig_handler先返回,那么SIGINT是不该被打印出来的,因为那时屏蔽字还没有恢复,所有信号都是不阻塞的。那么是Stevens说错了么?当然没有,只是Stevens没有说请在sigsuspend的原子操作中到底做了什么?
sigsuspend的整个原子操作过程为:
(1) 设置新的mask阻塞当前进程;
(2) 收到信号,恢复原先mask;
(3) 调用该进程设置的信号处理函数;
(4) 待信号处理函数返回后,sigsuspend返回。
大致就是上面这个过程,噢,原来signal handler是原子操作的一部分,而且是在恢复屏蔽字后执行的,所以上面的例子是没有问题的,Stevens说的也没错。由于Linux和Unix的千丝万缕的联系,所以在两个平台上绝大部分的系统调用的语义是一致的。上面的sigsuspend的原子操作也是从《深入理解Linux内核》一书中揣度出来的。书中的描述如下:
The sigsuspend( ) system call puts the process in the TASK_INTERRUPTIBLE state, after having blocked the standard signals specified by a bit mask array to which the mask parameter points. The process will wake up only when a nonignored, nonblocked signal is sent to it. The corresponding sys_sigsuspend( ) service routine executes these statements:

mask &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));
spin_lock_irq(¤t->sigmask_lock);
saveset = current->blocked;
siginitset(¤t->blocked, mask);
recalc_sigpending(current);
spin_unlock_irq(¤t->sigmask_lock);
regs->eax = -EINTR;
while (1) {
    current->state = TASK_INTERRUPTIBLE;
    schedule(  );
    if (do_signal(regs, &saveset))
        return -EINTR;
}
而最后的do_signal函数调用则是负责调用User Signal Handler的家伙。我想到这CU上的那个问题该被解疑清楚了吧。

理解Zombie和Daemon Process

潜水于CU(www.chinaunix.net),看到了大家对Zombie Process和Daemon Process的理解,同样也意识到以前自己对这两个概念理解的偏颇,想在这篇Blog中将之纠正。

一、Zombie Process
Zombie Process,译成中文为僵尸进程,以前我一直认为父进程先结束,子进程就变成了僵尸进程,事实上这与正确的理解恰恰相反,真惭愧,只是从字面理解了而并未深入研究。下面重新理解一下:

父子进程的退出次序无非两种:(这里的父进程并不等待子进程)
(1) 父进程先,子进程后
在《Unix环境高级编程》中Stevens是这样说的:“对于其父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程领养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程ID就更改为1 ( init进程的ID )。这种处理方法保证了每个进程有一个父进程”。这样子进程退出后的“善后”工作就由init进程来完成了,不会产生Zombie Process,在后面Stevens谈到了避免子进程成为Zombie Process的一个技巧就是利用init进程托管。

(2) 子进程先,父进程后
用CU上一个网友的形象理解就是“小孩死了老爸不管就变僵尸了”。其实进程的退出应该分成两个阶段:
   a) 进程主程序退出,此时进程进入TASK_ZOMBIE状态。此时大部分与该进程相关的资源都已被释放了,包括该进程的运行的地址空间已不存在了,它拥有的东西包括内核进程栈信息、线程相关信息等其父进程可能需要知道的信息。
   b) 当其父进程获取上述进程留下的信息后(调用wait or waitpid)或者其父进程通知内核对该进程的信息不感兴趣(调用signal(SIGCHLD,SIG_IGN); )时,该进程在内核中的资源才被释放。只有这两部都完成了,该进程才算是真正意义上的优美退出。而产生Zombie Process的本质就在于只完成了a)步骤,而b)的步骤却迟迟没有进程来完成(这本来是fork该子进程的父进程的责任)。这样的话,该进程在内核中占用的资源始终不能得到释放,一旦系统内部Zombie Process多了,系统运行就会受到影响了。

二、Daemon Process
Daemon Process,译为守护进程、后台进程或精灵进程。其定义这里引用Stevens的话“守护进程是生存期长的一种进程。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。他们常常在系统引导装入时启动,在系统关闭时终止。unix系统有很多守护进程,大多数服务器都是用守护进程实现的”。

守护进程可以通过一步一步的改造普通进程而得来。创建守护进程的步骤很固定,但是想要完全理解为什么要这么做的话,要了解的东西还不少。我们先来看看Stevens的做法:
int daemon_init(void) {
        int pid;
        pid = fork();             ———–(1)
        if (pid < 0) {
                return -1;
        } else if (pid > 0) {
                exit(0);
        }

        /* child process */
        setsid();          ———-(2) 注[1]

        chdir("/"); 

        umask(0);

        关闭相关文件描述符(根据具体的系统而定)

        return 0;
}
由于在书中Stevens对这些已经说的很详细,这里只是简单说明:
(1) 这里父进程退出,子进程为init进程托管,所以你用ps -fj察看会发现其ppid == 1。这里子进程从亲生父进程那继承了进程组ID、会话(session)ID和控制终端。子进程由于派生于父进程所以不可能成为进程组首进程,这为其成为Daemon创造了先天的条件(可以调用setsid成为新的session的首进程)。而后天的条件则需其自己创造了。
(2) 而子进程要想成为Daemon,就必须建立新的会话(Session)。由于会话对控制终端的独享性,一旦子进程创建了新的会话,就会自动脱离原先继承的控制终端。由于已经是新的会话所以进程组ID和Session ID都为该子进程的PID,该进程也成为新的进程组的首进程。

在CU的讨论中,又有如下一些问题:
a) 如何禁止进程重新打开控制终端?
现在,进程已经成为无终端的会话首进程,但它可以重新申请打开一个控制终端。如何来做来阻止其重新打开一个控制终端呢?可以通过使进程不再成为会话组长来禁止进程重新打开控制终端。这个话题有时被说成“创建一个Daemon进程到底需要一次fork还是二次fork”
int daemon_init(void) {
        int pid;
        pid = fork();            
        if (pid < 0) {
                return -1;
        } else if (pid > 0) {
                exit(0);
        }

        /* new session founder process */
        setsid();        

        pid = fork();
       if (pid < 0) {
          return -1;
       } else if (pid > 0) {
          exit(0);
       }

        /* child process */
        chdir("/"); 

        umask(0);

         关闭相关文件描述符(根据具体的系统而定)

        return 0;
}

b) 是否处理SIGCHLD信号?
很多Daemon进程在运行过程中还会fork出很多子进程,如果父进程不等待这些子进程,它们结束后就会变成Zombie Process,仍然占用了系统的资源,简单的调用signal(SIGCHLD, SIGIGN);就可以避免这种事情的发生,这个根据程序的需要可选。

三、总结
在实际的开发中,Zombie Process的产生往往是由于设计不当造成的。而创建Daemon Process也是不局限于上面Stevens的做法,当然必要的步骤是不能省略的。

四、参考资料
1、《Unix环境高级编程》
2、《深入理解Linux内核》

[注1]进程组、会话(Session)和控制终端(Control Terminal)之间的关系
理解Daemon Process涉及到进程组、会话(Session)和控制终端(Control Terminal)等多个概念,下面是它们的概念和之间的关系:
进程组:进程组是一个或多个进程的集合。每个进程组有一个唯一的进程组ID;
会话:一个或多个进程组的集合;
控制终端:通常是我们在登录的终端设备(终端登录情况)或伪终端设备(网络登录情况)。

一个会话若干个进程组(一般一个前台进程组和若干个后台进程组) 0或1个控制终端

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats