当前位置 : 主页 > 编程语言 > java >

【Linux Program】进程和信号

来源:互联网 收集:自由互联 发布时间:2022-06-23
系列文章: 文件操作 数据管理 进程和信号 POSIX 线程 进程间通信:管道 信号量共享内存和消息队列 套接字 进程的结构、类型和调度,用不同的方法启动新进程,父进程、子进程和僵

系列文章:

  • 文件操作
  • 数据管理
  • 进程和信号
  • POSIX 线程
  • 进程间通信:管道
  • 信号量共享内存和消息队列
  • 套接字

进程的结构、类型和调度,用不同的方法启动新进程,父进程、子进程和僵尸进程,什么是信号以及如何使用它们

什么是进程

UNIX 标准把进程定义为:一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源。

正在运行的程序或进程由程序代码、数据、变量(占用着系统内存)、打开的文件(文件描述符)和环境组成。一般来说,Linux系统会在进程之间共享程序代码和系统函数库,所以在任何时刻内存中都只有代码的一份副本。

进程的结构

如果有两个用户 neil 和 rick,他们同时运行 grep 程序在不同的文件中查找不同的字符串,他们使用的进程如图所示:

【Linux Program】进程和信号_linux

每个进程都会被分配一个唯一的数字编号,我们称之为进程标识符或PID。它通常是一个取值范围从2-32768的正整数。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的PID,当数字已经回绕一圈时,新的PID重新从2开始,数字1一般是为特殊进程init保留的,init进程复制管理其它进程。
【Linux Program】进程和信号_#include_02
将要被grep命令执行的程序代码被保存在一个磁盘文件中。正常情况下,Linux进程不能对用来存放程序代码的内存区域进行写操作,即程序代码是以只读方式加载到内存中的。

系统函数库也可以被共享。例如,不管有多个正在运行的程序要调用 printf 函数,内存中只要有它一份副本即可。

共享函数库带来的另一个优点是,包含可执行程序grep的磁盘文件容量比较小,因为它不包含共享函数库代码。对整个操作系统来说,把常用例程提取出来放入C语言的标准函数库中将节省大量的磁盘空间。

并不是程序在运行时所需的所有东西都可以被共享。例如,进程使用的变量就与其他进程所使用的的不同。这两个grep命令所使用的文件也各不相同,进程通过各自的文件描述符来访问文件。

除此之外,进程有自己的栈空间,用于保存函数中的局部变量和控制函数的调用与返回。进程还有自己的环境空间,包含专门为这个进程建立的环境变量(使用putenv和getenv函数操作这些环境变量),进程还必须维护自己的程序计数器,这个计数器用来记录它执行到的位置,即在线程中的位置。在使用线程时,进程可以有不止一个执行线程。

在许多Linux系统上,在目录/proc中有一些特殊的文件,这些文件的特殊之处在于它们允许你窥视正在运行的进程的内部情况,就好像这些进程是目录中的文件一样。

进程表

Linux 进程表就像一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的PID、进程的状态、命令字符串和其他一些ps命令输出的各类信息。操作系统通过进程的PID对它们进行管理,这些PID是进程表的索引。进程表的长度是有限制的,所以系统能够支持的同时运行的进程数也是有限制的。

查看进程

ps 命令可以显示我们正在运行的进程、其他用户正在运行的进程或者目前在系统上运行的所有进程。

【Linux Program】进程和信号_信号_03
这个命令显示了许多进程的相关信息,例如,tty一列显示了进程是从哪一个终端启动的,TIME一列是进程目前为止所占用的CPU时间,CMD一列显示启动进程锁使用的命令。默认情况下,ps程序只显示与终端、主控台、串行口或伪终端保持连接的进程的信息。其它进程在运行时不需要通过终端与用户进行通信,它们都是一些系统进程。可以使用 -a 选项来查看所有进程,用 -f 选项显示进程完整的信息。

系统进程

ps 命令输出中的 STAT 一列用来表明进程的当前状态。

STAT 代码

说明

S

睡眠。通常是在等待某个事件的发生,如一个信号或有输入可用

R

可运行。即在运行队列中,处于正在执行或即将运行状态

D

不可中断睡眠(等待)。通常是在等待输入或输出完成

T

停止。通常是被shell作业所停止,或者进程正处于调试器的控制之下

Z

死(Defunct)进程或僵尸(zombie)进程

N

低优先级任务,nice

W

分页。(不适用于2.6版本开始的Linux内核)

s

进程是会话期首进程

+

进程属于前台进程组

l

进程是多线程的

<

高优先级任务

一般而言,每个进程都是由另一个我们称之为父进程的进程启动的,被父进程启动的进程叫做子进程。Linux 系统启动时,它将运行一个名为 init 的进程,该进程的是系统运行的第一个进程,它的进程号为 1。你可以把 init 进程看作为操作系统的进程管理器,它是其他所有进程的祖先进程。我们将要看到的其他系统进程要么是由 init 进程启动的,要么是由被 init 进程启动的其他进程启动的。

用户登录的处理过程就是一个这样的例子,init 进程为每个用户用来登录的串行终端或拨号调制解调器启动一次getty程序。getty 进程等待来自终端的操作,向用户显示熟悉的登录提示符,然后把控制移交给登录程序,登录程序设置用户环境,最后启动一个shell。用户退出系统时,init 进程将再次启动另一个 getty 进程。启动新进程并等待它们介绍的能力是整个系统的基础。

进程调度

21475 pts/2 R+ 0:00 ps ax

这行表明进程21475处于运行状态(R),正在执行的命令是 ​​ps ax​​。这个状态指示符只是表明程序已准备好运行,并不意味着它正在运行。在一台单处理器计算机上,同一时间只能有一个进程可以运行,其它进程处于等待运行状态。每个进程轮到的运行时间(时间片)是相当短暂的,给人一种多个程序在同时运行的假象。状态R+只表示这个程序是一个前台任务,它不是在等待其他进程结束或等待输入输出操作完成。

Linux 内核用进程调度器来决定下一个时间片该分配给哪个进程。它的判断依据是进程的优先级。优先级高的进程运行得更为频繁。而其他进程,如低优先级的后台任务运行的就不是非常频繁。在Linux中,进程的运行时间不可能超过分配给它们的时间片,它们采用的是抢先式多任务处理,所以进程的挂起和继续运行无需彼此之间的协作。

在一个如Linux这样的多任务系统中,多个程序可能会竞争使用同一个资源。在这种情况下,执行短期的突发性工作并暂停运行来等待输入的的程序,要比持续占用处理器来进行计算或不断轮询系统来查看是否有新的输入到达的程序要更好。我们称表现良好的程序为nice程序,而且在某种意义上,这个nice值可以被计算出来的。操作系统根据进程的nice值来决定它的优先级,一个进程的nice值默认为0并将根据这个程序的表现而不断变化。长期不间断运行的程序的优先级一般会比较低。而暂停来等待输入的程序会得到奖励。这可以帮助与用户进行交互的程序保持及时的响应性。在程序等待用户的输入时,系统会增加它的优先级。可以用nice命令设置进程的nice值,使用renice命令调整它的值。nice 命令是将进程的nice值增加10,从而降低该进程的优先级。

Linux 调度器根据进程的优先级来决定运行哪个进程。每个系统的具体实现各有不同,但高优先级的进程总是运行得更频繁。在某些情况下,只要还有高优先级的进程可以运行,低优先级的进程就根本不能运行。

启动新进程

我们可以在一个程序的内部启动另一个程序,从而创建一个新进程。

#include <stdlib.h>

int system(const char *string);

system 函数的作用是,运行以字符串参数的形式传递给它的命令并等待该命令的完成。命令的执行情况就如同在shell中执行如下的命令:​​sh -c string​​。

#include <stdlib.h>
#include <stdio.h>

int main()
{
printf("Running ps with system\n");
system("ps ax");
printf("Done.\n");
exit(0);
}

因为system函数用一个shell来启动想要执行的程序,所以可以把这个程序放到后台执行。具体做法是 ​​system("ps ax &");​​。

system 函数很有用,但它也有局限性,因为程序必须等待由system函数启动的进程结束之后才能继续,因为我们不能立刻执行其他任务。在后者,对system函数的调用将在shell命令结束后立刻返回。由于它是一个在后台运行程序的请求,所以ps程序一启动shell就返回了。

使用system函数远非启动其他进程的理想手段,因为它必须用一个shell来启动需要的程序。

替换进程映像

​​exec​​ 系列函数由一组相关的函数组成,它们在进程的启动方式和程序参数的表达方式上各有不同。exec 函数可以把当前进程替换为一个新进程,新进程由 path 或 file 参数指定。可以使用 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[]);

这些函数可以分为两大类。execl、execlp 和 execle 的参数是可变的,参数以一个空指针结束。execv 和 execvp 的第二个参数是一个字符串数组。不管哪种情况,新程序在启动时会把在 argv 数组中给定的参数传递给 main 函数。

这些函数通常都是用 execve 实现的,虽然并不是必须这要做。

以字母p结尾的函数通过搜索PATH环境变量来查找新程序的可执行文件的路径。如果可执行文件不在PATH定义的路径中,我们就需要把包括目录在内的使用绝对路径的文件名作为参数传递给函数。

全局变量 environ 可用来把一个值传递到新的程序环境中。此外,函数 execle 和 execve 可以通过参数 envp 传递字符串数组作为新程序的环境变量。

如果想通过exec函数来启动ps程序,我们可以从6个exec函数中选择一个。

#include <unistd.h>

// Example of an argument list
char *const ps_argv[] = {"ps", "ax", 0};
// Example environment not terribly useful
char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", 0};

int main()
{
// 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);

execv("/bin/ps", ps_argv);
execvp("ps", ps_argv);
execve("/bin/ps", ps_argv, ps_envp);
}// pexec.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>


int main()
{
printf("Running ps with execlp\n");
execlp("ps", "ps", "ax", 0);
printf("Done.\n");
exit(0);
}

运行这个程序时,你会看到正常的ps输出,但字符串Done却根本没有出现,此外,ps 的输出中没有 pexec 进程的任何信息。程序先打印出它的第一条消息,接着调用 execlp,这个函数在 PATH 环境变量给出的目录中搜索程序 ps,然后用这个程序替换 pexec 程序。

ps 命令结束时,我们看到一个新的shell提示符,因为我们并没有再返回到 pexec 程序中,所以第二条消息是不会打印出来的。新进程的 PID、PPID 和 nice 值与原先的完全一样,运行中的程序开始执行 exec 调用中指定的新的可执行文件中的代码。

一般情况下,exec 函数是不会返回的,除非发生了错误。出现错误时,exec 函数将返回 -1,并且会设置错误变量 errno。

复制进程映像

要想让进程同时执行多个函数,我们可以使用线程或从原程序中创建以完全分离的进程,后者就像 init 的做法一样,而不像 exec 调用那样用新程序替换当前执行的线程。

我们可以通过调用 fork 创建一个新进程。这个系统调用复制当前进程,在进程表中创建一个新的表项,新表项中的许多属性与当前进程是相同的。新进程几乎与原进程一模一样,执行的代码也完全相同,但新进程有自己的数据空间、环境和文件描述符。fork 和 exec 函数结合在一起使用就是创建新进程所需要的一切了。

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

pid_t fork(void);

在父进程中的fork调用返回的是新的子进程PID。新进程将继续执行,就像原进程一样,不同之处在于,子进程中的 fork 调用返回的是 0。父进程可以通过这一点来判断就是谁是父进程,谁是子进程。

如果 fork 失败,它将返回 -1。失败通常是因为父进程拥有的子进程数目超过了规定的限制,此时 errno 将被设为 EAGAIN,如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足,errno 变量将被设为 ENOMEM。

一个典型的 fork 的代码片段:

pid_t new_pid;
new_pid = fork();

switch(new_pid)
{
case - 1: // Error
break;
case 0: // child
break;
default:
break;
}jiaming@jiaming-pc:~/Documents/test$ ./a.out
fork program starting
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
This is the child
This is the child

这个程序以两个进程的形式在运行。子进程被创建并且输出消息 5 次。原进程(即父进程)只输出消息 3 次。父进程在子进程打印它的全部消息之前就结束了,因此我们将看到在输出内容中混杂这一个 shell 提示符。

程序在调用 fork 时被分为两个独立的进程。程序通过 fork 调用返回的非零值确定父进程,并根据该值来设置消息的输出次数,两次消息的输出之间间隔一秒。

等待一个进程

当用 fork 启动一个子进程时,子进程就有了它自己的生命周期并将独立运行。有时,我们希望知道一个子进程何时结束。例如,在示例中,父进程在子进程之前结束,由于子进程还继续运行,所以得到的输出结果有点乱。我们可以通过在父进程中调用wait函数让父进程等待子进程的结束。

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

pid_t wait(int *stat_loc);

wait 系统调用将暂停父进程直到它的子进程结束为止。这个调用返回子进程的PID,它通常是已经结束运行的子进程的PID。状态信息允许父进程了解子进程的退出状态,即子进程的main函数返回的值或子进程中 exit 函数的退出码。

我们可以用 ​​sys/wait.h​​ 文件中定义的宏来解释状态信息。


说明

WIFEXITED(stat_val)

如果子进程正常结束,它就取一个非零值

WEXITSTATUS(stat_val)

如果 WIFEXITED 非零,它返回子进程的退出码

WIFSIGNALED(stat_val)

如果子进程因为一个未捕获的信号而终止,它就取一个非零值

WTERMSIG(stat_val)

如果WIFSIGNALED非零,它就返回一个信号代码

WIFSTOPPED(stat_val)

如果子进程意外终止,它就取一个非零值

WSTOPSIG(stat_val)

如果WIFSTOPPED非零,它返回一个信号代码

我们稍微修改一下程序,让父进程等待并检查子进程的退出状态。

#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);
}
exit(0);
}jiaming@jiaming-pc:~/Documents/test$ ./a.out
fork program starting
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
This is the child
This is the child
Child has finished: PID = 28282
Child exited with code 0

父进程(从fork调用中获得一个非零的返回值)用wait系统调用将自己的执行挂起,直到子进程的状态信息出现为止。这将发生在子进程调用 exit 的时候。我们将子进程的退出码设置为 37。父进程然后继续运行,通过测试 wait 调用的返回值来判断子进程是否正常终止如果是,就从状态信息中取出子进程的退出码。

僵尸进程

用 fork 来创建进程确实很有用,但你必须清楚子进程的运行情况。子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才结束。因此,进程表中代表子进程的表项不会立刻释放。虽然子进程不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来,以备父进程今后的wait调用使用。这时它将成为一个死进程或僵尸进程。

如果子进程输出消息的次数少于父进程,它就会率先结束并成为僵尸进程直到父进程也结束。如果此时父进程异常终止,子进程将自动把PID为1的进程作为自己的父进程。子进程现在是一个不再运行的僵尸进程,但因为其父进程异常终止,所以它由init进程接管。僵尸进程将一直保留在进程表中直到被init进程发现并释放。进程表越大,这一过程就越慢。应该尽量避免产生僵尸进程,因为在init清理它们之前,它将一直消耗系统的资源。

还有另一个系统调用可以用来等待子进程的结束,它是waitpid函数。你可以用它来等待某个特定进程的结束。

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

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

pid 参数指定需要等待的子进程的PID,如果它的值为-1,waitpid 将返回任一子进程的信息,与wait一样,如果stat_loc不是空指针,waitpid 将把状态信息写到它所指向的位置。option 参数可用来改变waitpid的行为,其中最有用的一个选项是 WNOHANG,它的作用是防止waitpid调用将调用者的执行挂起。你可以用这个选项来查找是否有子进程已经结束,如果没有,程序将继续执行。其他的选项和wait调用的选项相同。

因此,如果想让父进程周期性地检查某个特定的子进程是否已终止,就可以使用如下的调用方式:

​​waitpid(child_pid, (int *)0, WNOHANG);​​

如果子进程没有结束或者意外终止,它就返回 0,否则返回 child_pid。如果 waitpid 失败,它将返回-1并设置errno。失败的情况包括:没有子进程(errno设置为ECHILD)、调用被某个信号中断(EINTR)、选项参数无效(EINVAL)。

输入和输出重定向

已打开的文件描述符将在 fork 和 exec 调用之后保留下来,下面的例子从标准输入中读取数据,然后向标准输出写数据,同时在二者之间对数据做一些有用的转换。

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>

int main()
{
int ch;
while((ch = getchar()) != EOF)
{
putchar(toupper(ch));
}
exit(0);
}

/*
jiaming@jiaming-pc:~/Documents/test$ ./a.out
hello
HELLO
ABC
ABC
^C
jiaming@jiaming-pc:~/Documents/test$ sudo vim file.txt
[sudo] password for jiaming:
jiaming@jiaming-pc:~/Documents/test$ cat file.txt
this is the file, file.txt, it is all lower case.
jiaming@jiaming-pc:~/Documents/test$ ./a.out < file.txt # 利用shell的重定向功能,将一个文件的内容全部变为大写
THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.
*/#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
char *filename;
if(argc != 2)
{
fprintf(stderr, "usage: useupper file\n");
exit(1);
}
filename = argv[1];
// 重新打开标准输入,并再次检查有无错误发生,然后用 execl 调用 upper 程序
if(!freopen(filename, "r", stdin))
{
fprintf(stderr, "could not redirect stdin from file %s\n", filename);
exit(2);
}
execl("./upper", "upper", 0);
// 不要忘记execl会替换当前的进程,如果没有发生错误,剩下的这些语句将不会执行
perror("could not exec ./upper");
exit(3);
}

运行这个程序时,我们可以提供给它一个文件,让它把该文件的内容全部转换为大写,这项工作由 upper 完成,但它不处理文件名参数。我们并不需要upper程序的源代码。

==该程序用 freopen 函数先关闭标准输入,然后将文件流 stdin 与程序参数给定的文件名关联起来。==接下来,调用 execl 用 upper 程序替换掉正在运行的进程代码。因为已打开的文件描述符会在 execl 调用之后保留下来。

线程

Linux 系统中的进程可以互相协作、互相a发送消息、互相中断,甚至可以共享内存段。但从本质上来说,它们是操作系统内各自独立的实体,想要在它们之间共享变量并不是很容易。

在许多UNIX和Linux系统中都有一类进程叫做线程。涉及线程的编程是比较困难的,但在其它某些应用软件中又有很大的用处。在Linux(Unix)系统中编写线程程序并不像编写多进程程序那么常见,因为Linux中的进程都是非常轻量级的,而且编写多个互相协作的进程比编写线程要容易得多。

信号

信号是UNIX和Linux系统响应某些条件而产生的一个事件。接收到该信号的进程会响应地采取一些行动。我们用术语生成表示一个信号产生,使用术语捕获表示接收到一个信号。信号是由于某些错误而生成的,如内存段冲突、浮点处理器错误或非法指令等。它们由shell和终端处理器生成来引起中断,它们还可以作为在进程间传递消息或修改行为的一种方式,明确地由一个进程发送给另一个进程。无论何种情况,它们的编程接口是相同的。信号可以被生成、捕获、响应或忽略。

信号的名称是在头文件 signal.h 中定义。

信号名称

说明

SIGABORT

进程异常终止

SIGALRM

超时警告

SIGFPE

浮点运算异常

SIGHUP

连接挂断

SIGILL

非法指令

SIGINT

终端中断

SIGKILL

终止进程

SIGPIPE

向无进程的管道写数据

SIGQUIT

终端退出

SIGSEGV

无效内存段访问

SIGTERM

终止

SIGUSR1

用户定义信号1

SIGUSR2

用户定义信号2

如果进程接收到这些信号中的一个,但事先没有安排捕获它,进程将会立刻终止。通常,系统将生成核心转储文件 core,并将其放在当前目前下。该文件是进程在内存中的映像,它对程序的调试很有用处。

信号名称

说明

SIGCHLD

子进程已经停止或退出

SIGCONT

继续执行暂停进程

SIGSTOP

停止执行

SIGTSTP

终端挂起

SIGTTIN

后台进程尝试读操作

SIGTTOU

后台进程尝试写操作

SIGCHLD 信号对于管理子进程很有用。默认情况下,它是被忽略的。其余的信号会使接收它们的进程停止运行,但 SIGCONT 是个例外,它的作用是让进程恢复并继续执行。shell 脚本通过它来控制作业,但用户程序很少会用到它。

如果shell和终端驱动程序是按通常情况配置的话,在键盘上敲入中断字符(通常是Ctrl+C)就会向前台进程发送 SIGINT 信号,这将引起该程序的终止,除非它事先就安排了捕获这个信号。

如果想发送一个信号给进程,而该进程并不是当前的前台进程,就需要使用kill命令。该命令需要有一个可选的信号代码或信号名称和一个接收信号的目标进程的PID。

向运行在另一个终端上的 PID 为 512 的进程发送挂断信号:​​kill -HUP 512​​

kill 命令有一个有用的变体 killall,它可以给运行着某一命令的所有进程发送信号。并不是所有的UNIX系统都支持它,但 Linux 系统一般都有该命令。如果不知道某个进程的 PID,或者想给执行相同命令的许多不同进程发送信号,这条命令就很有用了。一种常见的用法是,通知 inetd 程序重新读取它的配置选项,使用下面命令 ​​killall -HUP inetd​​

程序可以用 signal 库函数来处理信号,

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

signal 是一个带有 sig 和 func 两个参数的函数。准备捕获或忽略的信号由参数 sig 给出,接收到指定的信号后将要调用的函数由参数 func 给出。信号处理函数必须有一个 int 类型的参数并且返回类型为 void。signal 函数本身也返回一个同类型的函数,或者使用下面两个特殊值之一来代替信号处理函数。

SIG_INGN

忽略信号

SIG_DFL

恢复默认行为

以下示例,响应用户敲入的 Ctrl+C 组合键,在屏幕上打印一条适当的消息而不是终止程序的运行,当用户第二次按下 Ctrl+C 时,程序将结束运行。

#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);
}
// main 函数的作用是,截获按下Ctrl+C组合键时产生的 SIGINT 信号,没有信号出现时,它会在一个无限循环中每隔一秒打印一条信息

int main()
{
(void)signal(SIGINT, ouch);
while (1)
{
printf("Hello World!\n");
sleep(1);
}
}

第一次按下Ctrl+C组合键会让程序作出响应,然后程序继续执行。再按下Ctrl+C组合键时,程序结束运行,因为SIGINT信号的处理方式已恢复为默认行为 —— 终止程序的运行。

jiaming@jiaming-pc:~/Documents/test$ ./a.out
Hello World!
Hello World!
Hello World!
^COUCH! - I got signal 2
Hello World!
Hello World!
^C
jiaming@jiaming-pc:~/Documents/test$

信号处理函数使用了一个单独的整数参数,它就是引起该函数被调用的信号代码。如果需要在同一个函数中处理多个信号,这个参数就很有用。在本例中,我们打印 SIGINT 的值,它的值在这个系统中恰好是 2,但不能过分依赖传统的信号数字值,而应该在新的程序中总是使用信号的名字。

程序中安排函数 ouch 来处理在按下 Ctrl+C组合键时所产生的的 SIGINT 信号。程序会在中断函数 ouch 处理完毕后继续执行,但信号处理方式已恢复为默认行为。当它接收到第二个 SIGINT 信号后,程序将采取默认的行为,即终止程序的运行。

如果想保留信号处理函数,让它继续响应用户的Ctrl+C组合键,我们就需要再次调用 signal 函数来重新建立它。 这会使信号在一段时间i内无法得到处理,这段时间从调用中断函数函数,到信号处理函数的重建为止。如果在这段时间内程序接收到第二个信号,它就会违背我们的意愿终止程序的运行。

sigaction 函数相比 signal 更推荐!

signal 函数返回的是先前对指定信号进行处理的信号处理函数的函数指针,如果未定义信号处理函数,则返回 SIG_ERR 并设置为一个正数值。如果给出的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号,errno 将被设置为 EINVAL。

发送信号

进程可以通过调用 kill 函数向包括它本身在内的其它进程发送一个信号。如果程序没有发送该信号的权限,对 kill 函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。这个函数和同名的 shell 命令完成相同的功能。

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

int kill(pid_t pid, int sig);

kill 函数把参数 sig 给定的信号发送给由参数 pid 给出的进程号所指定的进程,成功时返回 0。要想发送一个信号,发送进程必须拥有相应的权限。这通常意味着两个进程必须拥有相同的用户 ID(即你只能发送信号给属于自己的进程,但超级用户可以发送信号给任何进程)。

kill 调用会在失败时返回 -1 并设置 errno 变量。失败的原因可能是:给定的信号无效;发送进程权限不够;目标进程不存在。

信号向我们提供了一个有用的闹钟功能,进程可以通过调用 alarm 函数在经过预定时间后发送一个 SIGALRM 信号。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

alarm 函数用来在 seconds 秒之后安排发送一个 SIGALRM 信号。但由于处理的延时和时间调度的不确定性,实际闹钟时间将比预先安排的要稍微拖后一点儿。把参数 seconds 设置为 0 将取消所有已设置的闹钟请求。如果在接收到 SIGALRM 信号之前再次调用 alarm 函数,则闹钟重新开始计时。每个进程只能有一个闹钟时间。alarm 函数的返回值是以前设置的闹钟时间的余留秒数,如果调用失败则返回 -1。

为了说明 alarm 函数的工作情况,通过使用 fork、sleep 和 signal 来模拟效果。程序启动一个新的进程,专门用来在未来的某一时刻发送一个信号。

#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:/* constant-expression */
perror("fork failed");
break;
case 0:
sleep(5);
kill(getppid(), SIGALRM); // 告诉子进程在等待5秒后发送一个SIGALRM信号给它的父进程
exit(0);
default:
break;
}
printf("waiting for alarm to go off\n");
(void)signal(SIGALRM, ding); // 父进程通过一个signal调用安排好捕获SIGALRM信号的工作,然后等待它的到来。
pause();
if(alarm_fired)
printf("Ding!\n");
printf("done\n");
exit(0);
}jiaming@jiaming-pc:~/Documents/test$ ./a.out
alarm application starting
waiting for alarm to go off
<5 second pause>
Ding!
done

这个程序用到了一个新的函数 pause,它的作用很简单,就是把程序的执行挂起直到有一个信号出现为止。当程序接收到一个信号时,预设好的信号处理函数将开始运行,程序也将恢复正常的执行。

#include <unistd.h>
int pause(void);

当它被一个信号中断时,将返回 -1,并把errno 设置为 EINTR。当需要等待信号时,一个更常见的方法是使用稍后将要介绍的 sigsuspend 函数。

闹钟模拟程序通过 fork 调用启动新的进程。这个子进程休眠 5 秒后向其父进程发送一个 SIGALRM 信号。父进程在安排好捕获 SIGALRM 信号后暂停运行,直到接收到一个信号为止。

使用信号并挂起程序的执行是Linux程序设计中的一个重要部分。这意味着程序不需要总是在执行着。程序不必在一个循环中无休止地检查某个事件是否已发生,相反,它可以等待事件的发生。这在只有一个 CPU 的多用户环境中尤其重要,进程共享着一个处理器,繁忙地等待将会对系统的性能造成极大的影响。程序中信号的使用将带来一个特殊的问题:如果信号出现在系统调用的执行过程中会发生什么情况?答案是:视情况而定。一般来说,你只需要考虑慢系统调用,例如从终端读数据,如果在这个系统调用等待数据时出现一个信号,它就会返回一个错误。如果你开始在自己的程序中使用信号,就需要注意一些系统调用会因为接收到了一个信号而失败,这种错误情况可能是你在添加信号处理函数之前没有考虑到的。

在编写程序中处理信号部分的代码必须特别小心,因为在使用信号的程序中会出现各种各样的竞态条件,例如,如果想调用 pause 等待一个信号,可信号却出现在调用 pause 之前,就会使程序无限期地等待一个不会发生的事件。这些竞态条件都是一些对时间要求很苛刻的问题,许多编程新手都有这方面的烦恼,所以在检查和信号相关的代码时总是要非常小心。

一个健壮的信号接口

UNIX 规范推荐了一个更新和更健壮的信号编程接口:​​sigaction​​。

#include <signal.h>

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

sigaction 结构定义在文件 signal.h 中,它的作用是定义在接收到参数 sig 指定的信号后应该采取的行动,该结构至少应该包含以下几个成员:

void (*) (int) sa_handler;
sigset_t sa_mask;
int sa_flags;

sigaction 函数设置u与信号 sig 关联的动作。如果 oact 不是空指针,sigaction 将把原先对该信号的动作写到它指向的位置。如果 act 是空指针,则 sigaction 函数就不需要再做其他设置了,否则将在该参数中设置对指定i信号的动作。

与 signal 函数一样,sigaction 函数会在成功时返回 0,失败时返回 -1。如果给出 的信号无效或者试图对一个不允许被捕获或忽略的信号进行捕获或忽略,错误变量 errno 将被设置为 EINVAL。

在参数 act 指向的 sigaction 结构中,sa_handler 是一个函数指针,它指向接收到信号 sig 时将被调用的信号处理函数。它相当于前面见到的传递给函数 signal 的参数 func。我们可以将 sa_handler 字段设置为特殊特殊值 SIG_IGN 和 SIG_DFL,它们分别表示信号将被忽略或把对该信号的处理方式恢复为默认动作。

sa_mask 成员指定了一个信号集,在调用 sa_handler 所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中。这是一组将被阻塞且不会传递给该进程的信号。设置信号屏蔽字可以防止前面看到的信号在它的处理函数还未运行时就被接收到的情况。使用 sa_mask 字段可以消除这一竞态条件。

但是,由 sigaction 函数设置的信号处理函数在默认情况下是不会被重置的,如果希望获得类似前面用第二次 signal 调用对信号处理进行重置的效果,就必须在 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);
}
}

信号集

头文件 signal.h 定义了类型 sigset_t 和用来处理信号集的函数。sigaction 和其他函数将用这些信号集来修改进程在接收到信号时的行为。

#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 和 sigdelst 从信号集中增加或删除给定的信号(signo),它们在成功时返回 0,失败时返回 -1 并设置 errno。只有一个错误代码被定义,即当给定的信号无效时,errno 将设置为 EINVAL。

函数 sigismember 判断一个给定的信号是否是一个信号集的成员。如果是就返回1;如果不是就,就返回 0。

#include <signal.h>
int sigismember(sigset_t *set, int signo);

进程的信号屏蔽字的设置或检查o工作由函数 sigprocmask 来完成。信号屏蔽字是指当前i被阻塞的一组信号,它们不能被当前进程接收到。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

sigprocmask 函数可以根据参数 how 指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数 set 指定,而原先的信号屏蔽字保存到信号集 oset 中。

参数 how 的取值,

SIG_BLOCK

把参数 set 中的信号添加到信号屏蔽字中

SIG_SETMASK

把信号屏蔽字设置为参数 set 中的信号

SIG_UNBLOCK

从信号屏蔽字中删除参数 set 中的信号

如果参数 set 是空指针,how 的值就没有意义了,此时这个调用的唯一目的就是把当前信号屏蔽字的值保存到 oset 中。

如果 sigprocmask 成功完成,它将返回 0,如果参数 how 取值无效,它将返回 -1 并设置 errno 为 EINVAL。

如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态。程序可以通过调用函数 sigpending 来查看它阻塞的信号中有哪些正停留在待处理状态。

#include <signal.h>
int sigpending(sigset_t *set);

这个函数的作用是,将被阻塞的信号中停留在待处理状态的一组信号写到参数 set 指向的信号集中。成功时它将返回 0,否则返回 -1 并设置 errno 以表明错误的原因。如果程序需要处理信号,同时又需要控制信号处理函数的调用时间,这个函数就很有用了。

进程可以通过调用 sigsuspend 函数挂起自己的执行,直到信号集中的一个信号到达为止,相比 pause() 函数更通用。

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);

sigsuspend 函数将进程的屏蔽字替换为由参数 sigmask 给出的信号集合,然后挂起程序的执行。程序将在信号处理函数执行完毕后继续执行。如果接收到的信号终止了程序,sigsuspend 就不会返回;如果接收到的信号没有终止程序,sigsuspend 就返回 -1 并将 errno 设置为 EINTR。

1. sigaction 标志

用在 sigaction 函数里的 sigaction 结构中的 sa_flags 字段可以包含下面的取值,用于改变信号的行为。

SA_NOCLDSTOP

子进程停止时不产生 SIGCHLD 信号

SA_RESETHAND

将对此信号的处理方式在信号处理函数的入口处重置为 SIG_DFL

SA_RESTART

重启可中断的函数而不是给出 EINTR 错误

SA_NODEFER

捕获到信号时不将它添加到信号屏蔽字中

当一个信号被捕获时,SA_RESETHAND 标志可以用来自动清除它的信号处理函数,就如同我们在前面所看到的那样。

程序中使用的许多系统调用都是可中断的。也就是说,当接收到一个信号时,它们将返回一个错误并将 errno 设置为 EINTR,表明函数是因为一个信号而返回的。使用了信号的应用程序需要特别注意这一行为。如果 sigaction 调用中的 sa_flags 字段设置了 SA_RESTART 标志,那么在信号处理函数执行之后,函数将被重启而不是被信号中断。

一般的做法是,信号处理函数正在执行时,新接收到的信号将在该h处理函数的执行期间被添加到进程的信号屏蔽字。这防止了同一信号的不断出现引起信号处理函数的再次运行。如果信号处理函数是一个不可冲入的函数,在它结束对第一个信号的处理之前又让另一个信号再次调用它就有可能引起问题。但如果设置了 SA_NODEFER 标志,当程序接收到这个信号时就不会改变信号屏蔽字。

信号处理函数可以在其执行期间被中断并再次被调用。当返回第一次调用时,它能否继续正确操作是很关键的。这不仅仅是递归(调用自身)的问题,而是可重入的(可以安全地进入和再次执行)的问题。Linux 内核中,在同一时间负责处理多个设备的中断服务例程就需要是可重入的,因为优先级更高的中断可能会在同一段代码的执行期间“插入”进来。

所有未在涉及信号处理时,
【Linux Program】进程和信号_子进程_04

2. 常用信号参考

下面的信号默认动作都是异常终止进程,但进程的结束会传递到 wait 和 waitpid 函数中,从而表明进程是因为某个特定的信号而异常终止的。

信号名称

说明

SIGALRM

由 alarm 函数设置的定时器产生

SIGHUP

由一个处于非连接状态的终端发送给控制进程,或者由控制进程在自身结束时发送给每个前台进程

SIGINT

一般由终端敲入的 Ctrl+C 组合键或预先设置好的中断字符产生

SIGKILL

因为这个信号不能捕获或忽略,所以一般在 shell 中用它来强制终止异常进程

SIGPIPE

作为一个请求被发送,要求进程结束运行,UNIX 在关机时用这个信号要求系统服务停止运行。它是kill命令默认发送的信号

SIGUSR1, SIGUSR2

进程之间可以用这个信号进行通信,例如让进程报告状态信息等

下面的信号也会引起进程的异常h终止,但可能还会有一些与具体实现相关的其它动作,比如创建 core 文件等。

信号名称

说明

SIGFPE

由浮点运算异常产生

SIGILL

处理器执行了一条非法指令,通常是由一个崩溃的程序或无效的共享内存模块引起的

SIGQUIT

一般由终端敲入的Ctrl+\组合键或预先设置好的退出字符产生

SIGSEGV

段冲突

下面的信号被进程接收到的进程会被挂起。

信号名称

说明

SIGSTOP

停止执行(不能被捕获或忽略)

SIGTSTP

终端挂起信号,通常因Ctrl+Z组合键产生

SIGTTIN、SIGTTOU

shell 用这两个信号表明后台作业因需要从终端读取输入或产生输出而暂停运行

SIGCONT 信号的作用是重启被暂停的进程,如果进程没有暂停,则忽略该信号。

SIGCHLD 信号在默认情况下被忽略。


上一篇:《推动密码融合应用的研究与思考》
下一篇:没有了
网友评论