Linux 进程和信号

Linux 进程和信号

进程

进程概念

根据 UNIX 标准,把进程理解为包含一个或多个正在执行的线程以及它们执行所需系统资源的地址空间
一个正在运行状态的进程由 program code 、data、variables 等组成

进程表

进程表记录了当前正在运行的所有进程的信息,早期 UNIX 系统限制了进程表最多包含 256 个进程,现在的一些实现已经放开了限制,仅受可用内存空间的限制

进程调度器

在 Linux 内核中,进程调度器会根据进程优先级决定接下来选择哪一个进程获得时间片

进程的优先级由进程的 nice value 决定,nice value 默认为 0 ,nice 值越高优先级越低,该值受进程的行为影响。运行时间长且运行过程中不暂停的程序优先级比较低,暂停等待用户输入的程序会被提高优先级,当一个进程已经准备好 resume 时,它会获得比较高的优先级

创建新进程

#include <stdlib.h>

int system (const char *string);

system("ps ax"); // option 1
system("ps ax &"); // option 2

System 通过调用 shell 来间接创建新进程,因此通过 System 创建新进程的方法比较低效

第一种写法,意味着当前程序需要等待启动的新进程结束后才能往下执行
第二种写法为后台运行,只需要等待 shell 命令结束后就能往下执行,不需要等待 shell 创建的新进程结束

替换原进程

不只是启动新进程,exec 系列除了创建新进程还会替换当前进程, exec 系列函数比 system 更高效

#include <unistd.h>

char **environ;
int execl(const char *path, const char *arg0, ..., (char *)0);
int execlp(const char *file, const char *arg0, ..., (char *)0);
int execle(const char *path, const char *arg0, ..., (char *)0, char *const
envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

exec 底层都是通过调用 execve 来实现的,带有 p 后缀的 exec 系列函数会去搜索 PATH 环境变量去找到 file

#include <unistd.h>

/* Example of an argument list */

/* Note that we need a program name for argv[0] */
char *const ps_argv[] =
{“ps”, “ax”, 0};

/* Example environment, not terribly useful */
char *const ps_envp[] =
{“PATH=/bin:/usr/bin”, “TERM=console”, 0};

/* Possible calls to exec functions */
execl(“/bin/ps”, “ps”, “ax”, 0); /* assumes ps is in /bin */
execlp(“ps”, “ps”, “ax”, 0); /* assumes /bin is in PATH */
execle(“/bin/ps”, “ps”, “ax”, 0, ps_envp); /* passes own environment */

execv(“/bin/ps”, ps_argv);
execvp(“ps”, ps_argv);
execve(“/bin/ps”, ps_argv, ps_envp);

替换完原来的进程之后,进程 PID 、nice value、parent PID 都与原来相同,并且新进程会继承原来进程的一些特性,比如会继续包含原来进程中已打开的 file descriptors (在 close on exec flag 开启的情况下)且所有已打开的目录流在原程序中已经被关闭了

在 Linux 中,参数列表和环境信息的大小是有限制的, 限制在 128k bytes。在 POSIX 规范中限制至少是 4096 bytes

exec 系列函数通常情况下不会有返回值,除非产生错误,会返回 -1 并且设置 errno

复制原进程

#include <sys/types.h>
#include <unistd.h>

pid_t new_pid;
new_pid = fork();
switch(new_pid) {
    case -1 : /* Error */
        break;
    case 0 : /* We are child */
        break;
    default : /* We are parent */
        break;
}

fork 函数会将当前进程一模一样复制一份,新创建的进程称为 child process,原进程为 parent process,child process 拥有自己独立的 data space 、environment、file descriptors

fork 函数的返回值是子进程的 PID,在子进程中 fork 函数返回 0,若在父进程中 fork 返回了 -1 则说明复制新进程失败,通常是因为 CHILD_MAX 对于子进程个数的限制,这种情况下 errno 会被设置成 EAGAIN
也可能是因为没有足够的 space 在 process table 中新建 entry,这种情况下 errno 会被设置成 ENOMEM

等待进程

若是通过 fork 函数创建了新进程,创建的进程就自己独立运行了。在某些场景下父进程需要知道子进程何时结束,我们可以使用 wait 系统调用

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *stat_loc);

执行 wait 系统调用之后,父进程会挂起等待直到它的一个子进程终止。此时, wait 系统调用的返回值是停止的子进程的 PID ,且系统会将子进程的退出状态信息(包含 exit code)写入 stat_loc 指向的内存,父进程获取到子进程的 PID 之后就会继续往下执行

给出以下宏定义用于了解退出状态信息

WIFEXITED(stat_val)
如果子进程是正常终止返回不为零的值

WEXITSTATUS(stat_val)
如果 WIFEXITED 不为 0 就返回子进程的 exit code

WIFSIGNALED(stat_val)
如果子进程以 uncaught signal 异常终止则返回一个非 0 值

WTERMSIG(stat_val)
如果 WIFSIGNALED 不为 0 返回一个 signal number

WIFSTOPPED(stat_val)
如果子进程停止运行返回非 0

WSTOPSIG(stat_val)
如果 WIFSTOPPED 不为 0 返回一个 signal number

列一个实际的例子

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    pid_t pid;
    char *message;
    int n;
    int exit_code;

    printf("fork program starting\n");
    pid = fork();
    switch(pid)
    {
        case -1:
            perror("fork failed");
            exit(1);
        case 0:
            message = "This is the child";
            n = 5;
            exit_code = 37;
            break;
        default:
            message = "This is the parent";
            n = 3;
            exit_code = 0;
            break;
    }

    for(; n > 0 ; n--) {
        puts(message);
        sleep(1);
    }

    if( pid != 0 ) {
        int stat_val;
        pid_t child_pid;

        child_pid = wait(&stat_val);

        printf("Child has finished: PID = %d\n", child_pid);
        if(WIFEXITED(stat_val))
        {
            printf("Child exited with code %d\n", WEXITSTATUS(stat_val));

        } else {
            printf("Child terminated abnormally\n");
        }
    }
    exit(exit_code);
}

如果想要指定等待某个子进程可以考虑使用 waitpid

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *stat_loc, int options);

pid > 0 时,只等待进程 id 为 pid 的子进程,无视其它所有子进程的结束
pid = 0 时,等待同一个进程组中的任何子进程
pid = -1 时,等待任何一个子进程退出,没有任何限制,此时 waitpid 和 wait 的作用一模一样
pid < -1 时,等待一个指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值

参数 stat_loc 如果不是一个空指针,则终止进程的终止状态就存放在 stat_loc 所指向的单元
参数 stat_loc 如果是一个空指针,则表示父进程不关心子进程的终止状态

option 参数的几个可选值
WNOHANG 若由 pid 指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回 0
WUNTRACED 返回终止子进程信息和因信号停止的子进程信息
WCONTINUED 返回收到 SIGCONT 信号而恢复执行的已停止子进程状态信息

返回值

成功会返回结束运行的子进程的进程号,失败返回 -1 ,若返回 WNOHANG 则代表没有子进程退出返回 0

僵尸进程

当一个子进程终止之后,它与父进程的关系仍然存在。在 process table 中它的 entry 仍然存在直到父进程正常终止或调用 wait 才会释放,这个机制也是导致 zombie process(defunct) 出现的主要原因

当父进程异常终止之后,它的子进程仍然存留在 process table 中成为一个 zombie process ,此时它的父进程被自动划分给 init (PID 1),在 init 清理它之前,它会一直占用资源,这个清理的过程用时随着 process table 越大就越久

信号

信号概念

信号是由 Linux 操作系统生成的一个作为响应一些情况而生成的事件,进程通过接收这些信号进而做出一些行动

我们使用术语 raise 来表示信号的产生,使用术语 catch 来表示接收信号。信号是在出现某些情况下才会 raise 的,例如内存段冲突、浮点处理器错误、非法指令等。信号由 shell 或 terminal handlers 生成,以引起中断,也可以作为进程之间传递信息和修改行为的方式

信号的名称都被定义在头文件 signal.h 中

SIGABORT *Process abort
SIGALRM Alarm clock
SIGFPE *Floating-point exception
SIGHUP Hangup
SIGILL *Illegal instruction
SIGINT Terminal interrupt
SIGKILL Kill (can’t be caught or ignored)
SIGPIPE Write on a pipe with no reader
SIGQUIT Terminal quit
SIGSEGV *Invalid memory segment access
SIGTERM Termination
SIGUSR1 User-defined signal 1
SIGUSR2 User-defined signal 2
SIGCHLD Child process has stopped or exited.
SIGCONT Continue executing, if stopped.
SIGSTOP Stop executing. (Can’t be caught or ignored.)
SIGTSTP Terminal stop signal.
SIGTTIN Background process trying to read.
SIGTTOU Background process trying to write.

信号机制

当一个进程接收到了一个事先没有安排要去捕捉的信号时,程序会立刻停下来,通常还会在当前目录下生成一个核心转储文件(core dump), 称为 core ,core 保存了当前这个进程的内存镜像便于调试

有些信号会引起程序立即停止有些信号则不会,例如 SIGCONT ,收到这个信号会使进程 resume

对于 SIGINT 信号,若 shell 和 terminal driver 是通常的配置情况下,键入 interrupt character(Ctrl+C) 会发送 SIGINT 信号给前台进程,若前台进程没有设置 catch 这个信号就会停止运行

如果你想发送信号给指定的进程而不是当前的前台进程,可以使用 kill 命令

例如,发送 hangup 信号给 PID 512 进程

kill -HUP 512

如果你想给所有进程发送信号,可以使用

killall -HUP processName

killall 会给所有指定名称的进程发送指定的信号

在程序中我们可以通过 signal library function 来控制信号

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);

signal 函数有两个参数,一个是 sig,制定信号的类型(用于接收或捕获),当指定的信号被接收到时,会调用函数 func
func 必须带有一个 int 参数(用来存储信号),并且返回类型必须是 void

对于 func 可以有三种选择,第一种就是指向一个自定义的带有至少一个 int 参数的用来处理信号的处理函数的指针,另外两种选择分别是 SIG_IGN 和 SIG_DFL,分别代表了忽略当前信号和调用系统默认处理方式

signal 的返回值是指向之前的信号处理程序的指针,若发生错误也可能返回 SIG_ERR 并且设置相应的 errno(正值)

需要说明的是并不是所有的信号都可以被 catch 或者 ignore 的,例如 SIGKILL

简单的例子

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void ouch(int sig)
{
    printf("OUCH! - I got signal %d\n", sig);
    (void) signal(SIGINT, SIG_DFL);
}

int main()
{
    (void) signal(SIGINT, ouch);

    while(1) {
        printf("Hello World!\n");
        sleep(1);
    }
}

signal 中对应信号的处理程序执行结束之后,该信号的处理程序会被置为默认值

如果希望一直保持自定义的信号处理程序,需要在每次重置为默认值时重新调用 signal ,在 signal 重建信号处理程序的过程中,不会处理信号,在信号处理被重置为默认值且还未开始 signal 重建信号处理程序的时间内若收到 signal ,仍会按默认值来处理

使用 signal 来 catch 信号是不推荐的,建议的方法是调用 sigaction 来实现

发送信号

在程序中我们同样可以通过 kill 函数来发送信号,如果发送失败了通常是因为程序没有发送信号的权限,往往是因为目标进程属于其它用户

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

kill 函数发送 sig 信号给 pid 进程,若返回 0 则表示信号发送成功,发送信号的程序必要要有权限才能发送(指你只能发送信号给你当前用户 ID 下属的进程,若是 superuser 则可以发送信号给任意进程)

kill 如果发送失败,会返回 -1 并且设置 errno,若给定的信号无效 errno 会被设置为 EINVAL,若没有发送信号的权限,errno 会被设置为 EPERM,若目标进程不存在则 errno 会被设置为 ESRCH

Signals 提供了一个有用的闹钟功能,alarm 函数可以设置过多少时间发送 SIGALRM 信号(该信号默认情况下的处理方法是终止进程)

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

在 seconds 秒之后 alarm 会发送一个 SIGALRM 信号,若 seconds 值为 0 则会取消任何未完成的 alarm 请求,若在 alarm 还未发送 SIGALRM 信号之前再次触发 alarm,seconds 会被重置,每个进程只能有一个未完成的 alarm

alarm 会返回剩余触发 alarm 的时间,若调用 alarm 失败则返回 -1

// alarm.c

#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static int alarm_fired = 0;

void ding(int sig)
{
    alarm_fired = 1;
}

int main()
{
    pid_t pid;

    printf("alarm application starting\n");

    pid = fork();
    switch(pid) {
        case -1:
            /*Failure*/
            perror("fork failed");
            exit(1);
        case 0:
            /* child */
            sleep(5);
            kill(getppid(), SIGALRM);
            exit(0);
    }
    /* if we get here we are the parent process */
    printf("waiting for alarm to go off\n");
    (void) signal(SIGALRM, ding);

    pause();
    if (alarm_fired)
        printf("Ding!\n");

    printf("done\n");
    exit(0);

}

其中的 pause 函数会将当前程序暂停,直到接收到信号,signal handler 会被执行,程序也会恢复运行

#include <unistd.h>

int pause(void);

该函数只有一个返回值,可以理解为只有成功时会返回,返回 -1 同时 errno 的值置为 EINTR

如果信号的默认处理动作是终止进程,则进程终止, pause 没有机会返回 -1
如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause 函数不返回
如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause 返回 -1

更健壮的信号接口

X/Open 和 UNIX 标准推荐了一种更新的编程接口来代替 signal ,它就是 sigaction

#include <signal.h>

int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);

sigaction 结构用于定义信号被接收到之后的行为,它至少包含以下成员

void (*) (int) sa_handler /* function, SIG_DFL or SIG_IGN */
sigset_t sa_mask          /* signals to block in sa_handler */
int sa_flags              /* signal action modifiers */

act 参数指定新的信号处理方式,若 oact 不为 null,则先前的信号处理方式会被写入 oact 指向的内存

sigaction 执行成功返回 0 ,若失败返回 -1,errno 会被设置为 EINVAL

在 act 的结构体中,sa_handler 指向一个函数用于处理接收到 sig 的情况,也可以是 SIG_IGN 或 SIG_DFL

sa_mask 字段指定在调用 sa_handler 函数之前要添加到进程的信号掩码中的一组信号,这些信号会被屏蔽,不会被传递给进程

通过 sigaction 设置的 sa_handler 默认不会被重置,若要实现重置的效果,可以在 sa_flags 里设置 SA_RESETHAND

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void ouch(int sig)
{
    printf("OUCH! - I got signal %d\n", sig);
}

int main()
{
    struct sigaction act;

    act.sa_handler = ouch;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    sigaction(SIGINT, &act, 0);

    while(1) {
        printf("Hello World!\n");
        sleep(1);
    }
}

sigemptyset 用来将参数 set 信号集初始化并清空

sa_flags 可以包含以下值

SA_NOCLDSTOP 当子进程终止的时候不要生成 SIGCHLD 信号
SA_RESETHAND 收到信号后将信号行为重置为 SIG_DFL
SA_RESTART 重新启动可中断的函数而不是报错 EINTR
SA_NODEFER 当被捕获时,不要将信号添加到信号掩码中

补充一些细节,在 signal handler 执行的过程中该信号会被添加到当前的 signal mask 中(不这么做可能会重复调用 signal handler 导致出现问题) ,若设置了 SA_NODEFER 不会添加到 signal mask 中

一个 siganl handler 可能在中间被中断,然后被其他东西再次调用,在返回到第一个调用时,仍能正常运行是至关重要的,因此 signal handler 必须是可重入可中断的

以下列出一些在 signal handler 中可以安全调用的函数,这些函数是在 X/Open 规范下保证可重入的,或者本身不会发生信号

access alarm cfgetispeed cfgetospeed
cfsetispeed cfsetospeed chdir chmod
chown close creat dup2
dup execle execve _exit
fcntl fork fstat getegid
geteuid getgid getgroups getpgrp
getpid getppid getuid kill
link lseek mkdir mkfifo
open pathconf pause pipe
read rename rmdir setgid
setpgid setsid setuid sigaction
sigaddset sigdelset sigemptyset sigfillset
sigismember signal sigpending sigprocmask
sigsuspend sleep stat sysconf
tcdrain tcflow tcflush tcgetattr
tcgetpgrp tcsendbreak tcsetattr tcsetpgrp
time times umask uname
unlink utime wait waitpid write

信号集操作

头文件 signal.h 定义了类型 sigset_t 和一些处理信号集的函数

#include <signal.h>

int sigaddset(sigset_t *set, int signo);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigdelset(sigset_t *set, int signo);

sigemptyset 初始化信号集为空
sigfillset 设置信号集包含所有定义的信号
sigaddset 在指定的信号集中添加信号
sigdelset 在指定的信号集中删除信号

这些所有函数执行成功返回 0 执行失败返回 -1,并且设置 errno(EINVAL 指定的信号无效)

另外还有 sigismember 函数判断给定的信号是否是信号集的一个成员,若是成员返回 1,反之返回 0

#include <signal.h>

int sigismember(sigset_t *set, int signo);

sigprocmask 可以根据 how 参数以多种方式更改进程信号掩码,新的信号掩码在 set 参数中给出,当前的信号掩码记录在 oset 参数中(在 oset 指针非空的情况下)

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

how 参数可以有以下值

SIG_BLOCK 将 set 中的信号与当前的 signal mask 合并
SIG_SETMASK 将当前的 signal mask 设置成 set
SIG_UNBLOCK 在当前的 signal mask 中除去 set 中的信号

如果 set 参数为空,则 how 参数无意义,sigprocmask 此时只用于获取当前 signal mask 到 oset

成功返回 0 ,失败返回 -1,若 how 参数无效, errno 设置为 EINVAL

如果一个信号被 block 了,它虽然不会被传递给进程,但是它会被挂起。一个程序可以通过 sigpending 来确定它的哪些信号挂起了

#include <signal.h>

int sigpending(sigset_t *set);

sigpending 会将被阻塞的信号写入到 set 指向的信号集中,成功返回 0 ,失败返回 -1,errno 标志错误类型

如果在信号阻塞时将其发送给进程,那么该信号的传递就被推迟直到对它解除了阻塞。对应用程序而言,该信号好像发生在解除对 SIGINT 的阻塞和 pause 之间。如果发生了这种情况,或者如果在解除阻塞时刻和 pause 之间确实发生了信号,那么就产生了问题。因为我们可能不会再见到该信号,所以从这种意义上而言,在此时间窗口(解除阻塞和 pause 之间)中发生的信号丢失了,这样就使 pause 永远阻塞。

为了纠正此问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能是由 sigsuspend 函数提供的。

#include <signal.h>

int sigsuspend(const sigset_t *sigmask);

sigsuspend 函数替换当前进程的 signal mask 为 sigmask 并且暂停进程,当 signal handle 执行的时候当前进程会 resume,若收到的信号没有终止当前进程,sigsuspend 会返回 -1 并设 errno 为 EINTR

常见信号

以下列出一些默认行为是异常终止进程的信号(调用 _exit)

SIGALRM Generated by the timer set by the alarm function
SIGHUP Sent to the controlling process by a disconnecting terminal, or by the controlling process on termination to each foreground process
SIGINT Typically raised from the terminal by typing Ctrl+C or the configured interrupt character
SIGKILL Typically used from the shell to forcibly terminate an errant process, because this signal can’t be caught or ignored
SIGPIPE Generated by an attempt to write to a pipe with no associated reader
SIGTERM Sent as a request for a process to finish. Used by UNIX when shutting down to request that system services stop. This is the default signal sent from the kill command
SIGUSR1, SIGUSR2 May be used by processes to communicate with each other, possibly to cause them to report status information

以下列出的信号默认仍然异常终止进程,除此之外可能还会创建 core 文件

SIGFPE Generated by a floating-point arithmetic exception
SIGILL An illegal instruction has been executed by the processor. Usually caused by a corrupt program or invalid shared memory module
SIGQUIT Typically raised from the terminal by typing Ctrl+\ or the configured quit character
SIGSEGV A segmentation violation, usually caused by reading or writing at an illegal location in memory either by exceeding array bounds or dereferencing an invalid pointer. Overwriting a local array variable and corrupting the stack can cause a SIGSEGV to be raised when a function returns to an illegal address.

默认情况下,进程在收到下列信号时暂停

SIGSTOP Stop executing (can’t be caught or ignored)
SIGTSTP Terminal stop signal, often raised by typing Ctrl+Z
SIGTTIN, SIGTTOU Used by the shell to indicate that background jobs have stopped because they need to read from the terminal or produce output

SIGCONT 会重启一个已停止的进程,若没有停止的进程收到该信号会忽略
SIGCHLD 信号默认会被忽略,它在子进程停止或退出时会发送

参考资料

《Beginning Linux Programming》

Be the first to reply

发表评论

电子邮件地址不会被公开。 必填项已用*标注

15 − 13 =