Linux 进程执行简介

2015-11-05 kernel linux

简单介绍一下 Linux 中的常见的一些与进程相关的操作,主要是执行命令、守护进程等。

守护进程 (daemon)

没有终端限制,让某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。

守护进程的编程本身并不复杂,复杂的是各种版本的 Unix 的实现机制不尽相同,造成不同 Unix 环境下守护进程的编程规则并不一致。

守护进程的特性包括了:

  1. 后台运行;
  2. 与运行前的环境隔离开来,这些环境包括未关闭的文件描述符、控制终端、会话和进程组、工作目录以及文件创建 mask 等,这些环境通常是守护进程从执行它的父进程,特别是 shell,中继承下来的;
  3. 守护进程的启动方式有其特殊之处,可以从启动脚本 /etc/rc.d 中启动,crond 启动,还可以由用户终端执行。

编程步骤包括了。

1. 后台运行

创建子进程,父进程退出,在进程中调用 fork(),然后使父进程终止,让 Daemon 在子进程中后台执行,此时在形式上脱离了控制终端。

2. 脱离控制终端、登录会话和进程组

进程属于一个进程组,进程组号 (GID) 就是进程组长的进程号;会话可以包含多个进程组,这些进程组共享一个控制终端;这个控制终端通常是创建进程的登录终端。

控制终端、登录会话和进程组通常是从父进程继承下来的,我们的目的就是要摆脱它们,使之不受它们的影响。在此通过调用 setsid() 创建新的会话+进程组,并使当前进程成为会话组长。

注意,当进程是会话组长时 setsid() 调用失败,但第一步已保证该进程不是会话组长。调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

3. 禁止进程重新打开控制终端

现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端,可以通过使进程不再成为会话组长来禁止进程重新打开控制终端。

一般来说,也就是再次 fork() 一次,不过这个是可选的。

4. 关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符,如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。

一般需要关闭 0~2 标准输出。

5. 改变当前工作目录

进程活动时,其工作目录所在的文件系统不能卸下,一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 chdir("/tmp")

6.重设文件权限掩模

进程从创建它的父进程那里继承了文件权限掩模,它可能修改守护进程所创建的文件的存取位,为防止这一点,将文件创建掩模清除 umask(0)

注意,设置掩码时,使用的是八进制,例如 umask(022)

7. 处理 SIGCHLD 信号

处理 SIGCHLD 信号并不是必须的,但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程 (zombie) 从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。

在 Linux 下可以简单地将 SIGCHLD 信号的操作设为 SIG_IGN,signal(SIGCHLD, SIG_IGN),这样,内核在子进程结束时不会产生僵尸进程,这一点与 BSD4 不同,BSD4 下必须显式等待子进程结束才能释放僵尸进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>

int main(void)
{
    FILE *fp;
    time_t t;

    int pid;
    int i;

    printf("Before fork      , pgid=%d, ppid=%d, pid=%d, sid=%d\n",
            getpgid(getpid()), getppid(), getpid(), getsid(getpid()));

    pid = fork();   // STEP 1
    if (pid < -1) {        // error
        perror("fork()");
        exit(EXIT_FAILURE);
    } else if (pid != 0) { // parent
        fprintf(stdout, "Parent PID(%d) running\n", pid);
        exit(EXIT_SUCCESS);
    }

    printf("Forked before sid, pgid=%d, ppid=%d, pid=%d, sid=%d\n",
            getpgid(getpid()), getppid(), getpid(), getsid(getpid()));

    /* child */
    setsid();     // STEP 2, Detach from session.

    printf("Forked after sid , pgid=%d, ppid=%d, pid=%d, sid=%d\n",
            getpgid(getpid()), getppid(), getpid(), getsid(getpid()));

    pid = fork(); // STEP 3, fork again to prevent recreate a console.
    if (pid < -1) {        // error
        perror("fork()");
        exit(EXIT_FAILURE);
    } else if (pid != 0) { // parent
        fprintf(stdout, "Parent PID(%d) running\n", pid);
        exit(EXIT_SUCCESS);
    }

    printf("After second fork, pgid=%d, ppid=%d, pid=%d, sid=%d\n",
            getpgid(getpid()), getppid(), getpid(), getsid(getpid()));

    sleep(1);

    printf("Sleep for a while, pgid=%d, ppid=%d, pid=%d, sid=%d\n",
            getpgid(getpid()), getppid(), getpid(), getsid(getpid()));

    for(i = 0; i < getdtablesize(); i++) // STEP 4, close all opend file discript.
        close(i);

    chdir("/tmp");  // STEP 5
    umask(022);     // STEP 6

    while (1) {
        fp = fopen("test.log", "a");
        if(fp != NULL ) {
            t = time(0);
            fprintf(fp, "I'm here at %s\n", asctime(localtime(&t)) );
            fclose(fp);
        }
        sleep(60);
    }
}

可以通过如下命令查看当前进程的状态。

$ ps -axo pid,ppid,pgid,sid,state,comm | grep test

实际上,在上述的第二步中,可以再执行一次 fork() 操作。

第一次 fork() 后子进程继承了父进程的进程组 ID,但有一个新进程 ID,这就保证了子进程不是一个进程组的首进程。

然后 setsid() 是为了跟主进程的 SID PGID 脱离设置成子进程的 SID PGID,此时其父进程是 1 ,SID PGID 均等于 PID 。

虽然此时子进程已经被 init 接管了,但是只有 setsid() 之后才算是跟那个主进程完全脱离,不受他的影响 (原进程组、会话组被 kill 后该进程不会退出)。

第二次 fork()

第二次 fork() 不是必须的,主要目的是为了防止进程再次打开一个控制终端(暂时不知道如何打开)。

因为打开一个控制终端的前提条件是该进程必须是会话组长,那么再 fork() 一次后,子进程 ID 不再等于 sid (sid 是进程父进程的 sid),所以也无法打开新的控制终端。

此时这个子进程是首进程了,然后此时为了避免他是首进程,所以又 fork() 了一次,此时其父进程是上次 fork() 的进程,当父进程退出后其父进程变为 1 。

及时已经关闭所有的文件描述符,那么打印消息时仍然会打印到原终端。

防止出现僵尸进程

另外一种说法是为了防止出现僵尸进程。

每次调用父进程必须要保证可以快速退出,否则会导致子进程的父进程 ID 仍然为原进程,如果此时子进程先退出就会成为僵尸进程,即使已经调用了 setsid()

无论如何都必须要保证父进程的快速推出,否则不管是 fork 了几次,仍然会出现僵尸进程。