进程和信号构成了linux操作环境的基础部分。它们控制着linux和所有其他unix计算机系统执行的所有活动。不管是对于系统程序员、应用程序员还是系统管理员,理解linux和unix系统的进程管理是很有好处的。
下面主要介绍以下几方面的内容:
1 进程的结构、类型和调度
2 用不同的方法启动新进程
3 父进程、子进程和僵尸进程
4 什么是信号以及如何使用它们
什么是进程?
UNIX标准把进程定义为:“一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源。”可以把进程看作是正在运行的程序。
像linux这样的多任务操作系统可以同时运行多个程序。每个运行着的程序实例就构成一个进程。在X视窗系统等视窗化系统中这一特点尤为明显。如同微软的Windows系统,X视窗系统提供了一个图形化的用户界面,它允许同时运行多个应用程序,每个应用程序可以在一个或多个窗口中显示。
作为多用户系统,linux允许许多用户同时访问系统。每个用户可以同时运行许多个程序,甚至同时运行同一个程序的许多个实例。系统本身也运行着一些管理系统资源和控制用户访问的程序。
正在运行的程序或进程由程序代码、数据、变量(占用着系统内存)、打开的文件(文件描述符)和环境组成。一般来说,linux系统会进程之间共享程序代码和系统函数库,所以在任何时刻内存中都只有代码的一份副本。
进程的结构
每个进程都会被分配一个唯一的数字编号,称为进程标识符或PID.通常是一个取值范围从2到32768的正整数。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的PID,当数字已经回绕一圈时,新的PID重新从2开始。数字1一般是特殊进程init保留的,init进程负责管理其他进程。
正常情况下,linux进程不能对用来存放程序代码的内存区域进行写操作,即程序代码只是以只读方式加载到内存中的。虽然不能对这个区域执行写操作,但它可以被多个进程安全地共享。
系统函数库也可以被共享。如:不管有多少个正在运行的程序要调用printf函数,内存中只要有它的一个副本就好。当然并不是程序正在运行时所需要的所有东西都可以被共享。如:进程使用的变量与其他进程所使用的就不同。进程通过各自的文件描述符来访问文件。
进程有自己的栈空间,用于保存函数中的局部变量和控制函数的调用与返回。进程还有自己的环境空间,包含专门为这个进程建立的环境变量。进程还必须维护自己的程序计数器,这个计数器用来记录它执行到的位置,即在执行线程中的位置。在使用线程时,进程可以有不只一个执行线程。
在许多Linux系统上,在/proc中有一组特殊的文件,这些文件的特殊之处在于它们允许你查看正在运行的进程的内部情况,就好像这些进程是目录中的文件一样。
linux和unix一样,有一个虚拟内存系统,能够把程序代码和数据以内存页面的形式放到硬盘的一个区域中,所以linux可以管理的进程比物理内存能容纳得要多得多。
进程表
linux进程表就像一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的PID、进程的状态、命令字符串和其他一些ps命令输出的各类信息。操作系统通过进程的PID对它们进程管理,PID是进程表的索引。进程表的长度是有限制的,所以系统能够支持的同时运行的进程数也是有限制的。可以同时运行的进程数可能只与用于建立进程表项的内存容量有关,而没有具体的数字限制了。
查看进程
ps命令可以显示哦我们正在运行的进程,其他用户正在运行的进程或者目前在系统上运行的所有进程
系统进程
ps ax可以看到进程的状态(STAT)
STAT代码 | 说明 |
S | 睡眠,通常是在等待某个事件的发生,如一个信号或有输入可用 |
R | 运行,处于正在执行或将运行状态 |
D | 不可中断的睡眠(等待)。通常是等待输入或输出完成。 |
T | 停止。通常是被shell作业控制所停止,或者进程正处于调试器的控制之下。 |
Z | 死进程或僵尸进程 |
N | 低优先级任务 |
s | 进程是会话期首进程 |
+ | 进程属于前台进程组 |
l | 进程是多线程的 |
< | 高优先级任务 |
一般而言,每个进程都是由另一个称为父进程的进程启动的,被父进程启动的进程叫做子进程。linux系统启动时,它将运行一个名为init的进程,该进程是系统运行的第一个进程,它的进程号为1.可以把init进程看作为操作系统的进程管理器,它是其他所有进程的祖先进程。我们将要看到的其他系统进程要么是由init进程启动的,要么是由被init进程启动的其他进程启动的。
init进程为每个用户用来登录的串行终端或拨号调制解调器启动一次getty程序。gettty进程等待来自终端的操作,向用户显示熟悉的登录提示符,然后把控制移交给登录程序,登录程序设置用户环境,最好启动一个shell。用户退出系统时,init进程将再次启动另一个gettty进程。
启动新进程并等待它们结束的能力是整个系统的基础。下面将介绍如何从自己的程序中用系统调用fork、exec和wait来完成任务。
进程调度
在一个如linux这样的多任务系统中,多个程序可能会竞争使用同一个资源。在这种情况下,执行短期的突发性工作并暂停运行来等待输入的程序,要比持续占用处理器来进行计算或不断轮询系统来查看是否有新的输入到达的程序要更好。我们称表现良好的程序为nice程序,而且在某种意义上,这个nice是可以被计算出来的。操作系统根据进程的nice值来决定它的优先级,一个进程的nice值默认为0并将根据这个程序的表现而不断变化。长期不间断运行的程序的优先级一般会比较低。
启动新进程
我们可以在一个程序的内部启动另一个程序,从而创建一个新进程。这个工作可以通过库函数system来完成。
#include <stdlib.h>
int system(const char * string)
system函数的作用是运行以字符串参数的形式传递给它的命令并等待该命令的完成。命令的执行情况就像在shell中执行一样。
bash -c string
如果无法启动shell来运行这个命令,system函数将返回错误代码127;如果是其他错误,则返回-1。否则,system函数将返回该命令的退出码。
实验:
/* * system.c * * Created on: Jul 31, 2013 * Author: root */ #include <stdlib.h> #include <stdio.h> int main(int argc, char **argv) { printf("Running ps:"); system("ps ax"); printf("Done\n"); return 0; }
编译运行
[root@localhost C_test]# gcc -o system system.c [root@localhost C_test]# ./system PID TTY STAT TIME COMMAND 1 ? Ss 0:01 /sbin/init 2 ? S 0:00 [kthreadd] 3 ? S 0:00 [migration/0] 4 ? S 0:00 [ksoftirqd/0] 5 ? S 0:00 [migration/0] 6 ? S 0:00 [watchdog/0] 7 ? R 0:01 [events/0] 8 ? S 0:00 [cgroup] 9 ? S 0:00 [khelper] 10 ? S 0:00 [netns] 11 ? S 0:00 [async/mgr] 12 ? S 0:00 [pm] 13 ? S 0:00 [sync_supers] 14 ? S 0:00 [bdi-default]
system函数用一个shell来启动想要执行的程序,所以可以把这个程序放到后台执行。具体做法是将system.c中的函数调用修改为下面这样:
system("ps ax &");
程序以字符串ps ax为参数调用system函数从而在程序中执行ps命令。我们的程序在ps命令完成后从system调用中返回。system函数很有用,但它也有局限性,因为程序必须等待由system函数启动的进程结束之后才继续,因此我们不能立刻执行其他任务。
编译并运行这个新版本的程序
[root@localhost C_test]# ./system
Running ps:Done.
[root@localhost C_test]# PID TTY STAT TIME COMMAND
1 ? Ss 0:01 /sbin/init
2 ? S 0:00 [kthreadd]
3 ? S 0:00 [migration/0]
4 ? S 0:00 [ksoftirqd/0]
5 ? S 0:00 [migration/0]
6 ? S 0:00 [watchdog/0]
这个例子中,对system函数的调用将在shell命令结束后立即返回。由于它是一个在后台运行程序的请求,所以ps程序一启动shell就返回了,与我们在shell提示符下执行下面这条命令一样
$ ps ax &
新程序在未来得及输出所有结果之前,就打印出字符串Done然后退出了。在程序退出后,ps命令继续完成它的输出。这类的处理行为往往会给用户带来很大的困惑。
使用system函数远非启动其他进程的理想手段,因为它必须用一个shell来启动需要的程序。由于在启动程序之前需要先开启一个shell,而且对shell的安装情况及使用的环境的依赖也很大,所以使用system函数的效率不高。
替换进程映像
exec系列函数由一组相关的函数组成,它们在进程的启动方式和程序参数的表达方式上各有不同。wxec函数可以把当前进程替换为一个新进程,新进程由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函数。
以字母p结尾的函数通过搜索PATH环境变量来查找新程序的可执行文件的路径。如果可执行文件不在PATH定义的路径中,我们就需要把包括在目录在内的使用绝对路径的文件名作为参数传递给函数。
全局变量environ可用来把一个值传递到新的程序环境中。此外,函数execle和execve可以通过参数envp传递字符串数组作为新程序的环境变量。
如果想通过exec函数来启动ps程序,我们可以从6个exec函数中选择一个
#include <unistd.h>
/* 6个进程函数范例 */
char * const ps_argv[]={"ps","ax",0};
char * const ps_envp[]={"PATH=/bin:/usr/bin","TERM=console",0};
execl("/bin/ps","ps","ax",0);
execlp("ps","ps","ax",0); //以p结尾的函数会搜索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);
execlp函数使用
/* * system.c * * Created on: Jul 31, 2013 * Author: root */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { printf("Running ps with execlp\n"); execlp("ps","ps","ax", 0); printf("Done\n"); return 0; }
运行
[root@localhost C_test]# ./execlp
Running ps with execlp
PID TTY STAT TIME COMMAND
1 ? Ss 0:02 /sbin/init
2 ? S 0:00 [kthreadd]
3 ? S 0:00 [migration/0]
4 ? S 0:00 [ksoftirqd/0]
5 ? S 0:00 [migration/0]
6 ? S 0:00 [watchdog/0]
7 ? S 0:02 [events/0]
8 ? S 0:00 [cgroup]
9 ? S 0:00 [khelper]
10 ? S 0:00 [netns]
11 ? S 0:00 [async/mgr]
12 ? S 0:00 [pm]
13 ? S 0:00 [sync_supers]
14 ? S 0:00 [bdi-default]
15 ? S 0:00 [kintegrityd/0]
16 ? S 0:01 [kblockd/0]
17 ? S 0:00 [kacpid]
18 ? S 0:00 [kacpi_notif
看到正常的ps输出,但字符串Done没有出现,另外注意的是,ps的输出中没有execlp进程的任何信息。
程序先打印出它的第一条信息,接着调用execlp(函数),这个函数在PATH环境变量给出的目录中搜索程序ps。然后这个程序替换execlp(程序名),好像直接使用shell命令:ps ax一样。
ps命令结束时,我们看到一个新的shell提示符,因为我们并没有再返回到execlp程序中,所以第二条信息是不会打印出来的。新进程的PID PPID和nice值与原来的完全一样。运行中的程序开始执行exec调用中指定的新的可执行文件中的代码。
对于由exec函数启动的进程来说,它的参数表和环境加在一起的总长度是有限制的。上限由ARG_MAX给出,在linux系统上是128K字节。其他系统可能会设置一个非常有限的长度,这有可能会导致出现问题。
一般情况下,exec函数是不会返回的,除非发生了错误。出现错误时,exec函数将返回-1,并且会设置错误变量errno。
由exec启动的新进程继承了原进程的许多特性。在原进程中已打开的文件描述符在新进程中仍将保持打开,除非它们的“执行时关闭标志”被置位。任何在原进程中已打开的目录流都将在新进程中被关闭。
复制进程映像
要想让进程同时执行多个函数,我们可以使用线程或从原程序中创建一个完全分离的进程,后者就像init的做法一样,而不像exec调用那样用新程序替换当前执行的线程。
我们可以通过调用fork创建一个新进程。这个系统调用复制当前进程,在进程表中创建一个新的表项,新表项中的许多属性与当前进程是相同的。新进程几乎与原进程一样,执行的代码也完全相同,但新进程有自己的数据空间、环境和文件描述符。fork和exec函数结合在一起使用就是创建新进程所需要的一切了。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
如图所示,在父进程中的fork调用返回的是新的子进程的PID。新进程将继续执行,就像原进程一样,不同之处在于,子进程中的fork调用返回的是0。父子进程可以通过这一点来判断谁是父进程,谁是子进程。
如果fork失败,它将返回-1。失败通常是因为父进程所拥有的子进程数目超过了规定的限制(CHILD_MAX),此时errno将被设为EAGAIN。如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足,errno变量将被设为ENOMEM。
一个典型的使用fork的代码片段如下所示:
pid_t new_pid;
new_pid = fork();
switch(new_pid)
{
case -1:
break;
case 0:
break;
default:
break;
}
实验:fork函数
/* * system.c * * Created on: Jul 31, 2013 * Author: root */ #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { pid_t pid; char * message=NULL; int n=0; 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; break; default: message="This is the parent"; n=3; break; } for(;n>0;n--) { puts(message); //printf("%s",message); sleep(1); } return 0; }
运行:
[root@localhost C_test]# ./fork1
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
[root@localhost C_test]# 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函数的退出码。如果stat_loc不是空指针,状态信息将被写入它指向的位置。
我们可以用sys/wait.h文件中定义的宏来解释状态信息
宏 | 说明 |
WIFEXITED(stat_val) | 如果子进程正常结束,它就取一个非零值。 |
WEXITSTATUS(stat_val) | 如果WIFEXITED非零,它返回子进程的退出码 |
WIFSIGNALED(stat_val) | 如果子进程因为一个未捕获的信号而终止 |
WTERMSIG(stat_val) | 如果WIFSIGNALED非零,它返回一个信号代码 |
WIFSTOPPED(stat_val) | 如果子进程意外终止,它就取一个非零值 |
WSTOPSIG(stat_val) | 如果WIFSTOPPED非零,它返回一个信号代码 |
实验: wait函数
/* * system.c * * Created on: Jul 31, 2013 * Author: root */ #include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char **argv) { pid_t pid; char * message=NULL; int n; int exit_code; printf("fork program starting\n"); pid=fork(); switch(pid) { case -1: perror("fork failed"); exit(1); case 0: message="Child"; n=5; exit_code=2; break; default: message="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); }
运行
[root@localhost C_test]# ./wait
fork program starting
parent
Child
parent
Child
parent
Child
Child
Child
child has finished:PID=3489
Child exited with code 2
父进程用wait系统调用将自己的执行挂起,直到子进程的状态信息出现为止。这将发生在子进程调用exit的时候。我们将子进程的退出码设置为2。父进程然后继续运行,通过测试wait调用的返回值来判断子进程是否正常终止。如果是,就从状态信息中提取出子进程的退出码。
僵尸进程
用fork来创建进程确实有用,但必须清楚子进程的运行情况。子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才结束。进程表中代表子进程的表项不会立刻释放。虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来,以备父进程今后的wait调用使用。这时它将成为一个死(defunct)进程或僵尸(zombie)进程。
如果修改fork示例程序中的消息输出次数,我们就能看到僵尸进程。如果子进程输出消息的次数少于父进程,它就会率先结束并成为僵尸进程直到父进程也结束。
实验:僵尸进程
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> /*改变父子进程消息的次数而已*/ int main(int argc, char **argv) { pid_t pid; char * message=NULL; int n=0; printf("fork program starting\n"); pid=fork(); switch(pid) { case -1:perror("fork failed"); exit(1); case 0: message="This is the child"; n=3; break; default: message="This is the parent"; n=5; break; } for(;n>0;n--) { puts(message); // printf("%s",message); sleep(1); } return 0; }
如果用./fork2 & 命令来运行上面这个程序,然后在子进程结束之后父进程结束之前调用ps程序。
我们将会看到如下一行(彩色)
[root@localhost C_test]# ps -al | grep fork2
0 S 0 4150 3312 0 80 0 - 467 - pts/0 00:00:00 fork2
1 S 0 4151 4150 0 80 0 - 467 - pts/0 00:00:00 fork2
[root@localhost C_test]# ps -al | grep fork2
0 S 0 4150 3312 0 80 0 - 467 - pts/0 00:00:00 fork2
1 Z 0 4151 4150 0 80 0 - 0 - pts/0 00:00:00 fork2 <defunct>
如果此时父进程异常终止,子进程将自动把PID为1(init)的进程作为自己的父进程。子进程现在是一个不再运行的僵尸进程,但因为其父进程异常终止,所以它由init进程接管。僵尸进程将一直保留在进程表中直到被init进程发现并释放。进程表越大,这一过程就越慢。应该尽量避免产生僵尸进程,因为在init清理它们之前,它们将一直消耗系统的资源。
还有一个系统调用可用来等待子进程的结束,可以用它来等待某个特定进程的结束。
#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调用之后保留下来,我们可以利用对进程这方面知识的理解来改变程序的行为。下个例子涉及一个过滤程序:它从标准输入读取数据,然后向标准输出写数据,同时在输入和输出之间对数据做一些有用的转换。
实验:重定向
/* * upper.c * * Created on: Aug 2, 2013 * Author: root */ #include <stdio.h> #include <ctype.h> #include <stdlib.h> int main(int argc, char **argv) { int ch; while((ch=getchar())!=-1) { putchar(toupper(ch)); } return 0; }
运行
[root@localhost C_test]# ./upper < yao.txt HELLO WORLD YAO XIA BING
这里利用shell的重定向把一个文件的内容全部转换为大写
如果我想在另一个程序中使用这个过滤程序会怎么样?下面这个程序useupper.c接受一个文件名作为命令行参数,如果对它的调用不正确,它将响应一个错误信息。
实验:
/* * useupper.c * * Created on: Aug 2, 2013 * Author: root */ #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",NULL); //execl会替换掉当前的进程,如果没有发生错误,下面的语句不会被执行 perror("could not exec ./upper"); exit(3); }
运行
[root@localhost C_test]# gcc -o useupper useupper.c [root@localhost C_test]# ./useupper yao.txt HELLO WORLD YAO XIA BING
运行这个程序时,我们可以提供给它一个文件,让它把该文件的内容全部转换为大写。这项工作由程序upper完成。但它不处理文件名参数。useupper程序用freopen函数先关闭标准输入,然后将文件流stdin与程序参数给定的文件名关联起来。接下来,它调用execl用upper程序替换掉正在运行的进程代码。因为已打开的文件描述符会在execl调用之后保留下来,所以upper程序的运行情况和它在shell提示符下的运行情况完全一样:
线程
linux系统中的进程可以互相协作、互相发送消息、互相中断,甚至可以共享内存段。但从本质上来说,它们是操作系统内各自独立的实体,要想在它们之间共享变量并不是很容易。
在许多unix和linux系统中都有一类进程叫做线程(thread)。涉及线程的编程比较困难,但它在某些应用软件(如多线程数据库服务器)中又有很大用处。在linux系统中编写线程程序并不像编写多进程程序那么常见,因为linux中的进程都是非常轻量级的,而且编写多个互相协作的进程比编写线程要容易得多。
信号
信号是unix和linux系统响应某些条件而产生的一个事件。接收到该信号的进程会相应地采取一些行动。我们用术语生成(raise)表示一个信号的产生,使用术语捕获(catch)表示接收到一个信号。信号是由于某些错误条件而生成的,如内存段冲突、浮点处理器错误或非法指令等。它们由shell和终端处理器生成来引起中断,它们还可以作为在进程间传递消息或修改行为的一种方式,明确地由一个进程发送给另一个进程。无论何种情况,它们的编程接口都是相同的。信号可以被生成、捕获、响应或忽略。
信号的名称是在头文件signal.h中定义的。它们以SIG开头
信号名称 | 说明 |
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一般需要用ps命令查出来)。例如,如果向运行在另一个终端上的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_IGN 忽略信号
SIG_DFL 恢复默认行为
通过一个实例可以更清楚地理解信号的处理方法。它将响应用户敲入Ctrl+C组合键,在屏幕上打印一条适当的消息而不是终止程序的运行。当用户第二次按下Ctrl+C时,程序将结束运行。
实验:信号处理
/* * ctrlcl.c * * Created on: Aug 2, 2013 * Author: root */ #include <signal.h> #include <stdio.h> #include <unistd.h> /*函数ouch对通过参数sig传递进来的信号作出响应。信号出现时,程序调用该函数,它先打印一条消息, * 然后将信号SIGINT(默认情况下,按下CTRL+C将产生这个信号)的处理 * */ void ouch(int sig) { printf("OUCH! the signal is %d\n",sig); (void)signal(SIGINT,SIG_DFL); } /*main函数作用是截获按下CTRL+C组合键产生的SIGINT信号。没有 * 信号出现时,它会在一个无限循环中每隔一秒打印一条消息 * */ int main(int argc, char **argv) { (void)signal(SIGINT,ouch); while(1) { printf("Hello!\n"); sleep(1); } }
运行
[root@localhost C_test]# ./ctrlcl Hello! Hello! Hello! ^COUCH! the signal is 2 Hello! Hello! Hello! ^C
第一次按下CTRL+C组合键让程序作出响应,然后程序继续执行。再次按下CTRL+C组合键时,程序将结束运行,因为SIGINT信号的处理方式已恢复为默认行为--终止程序的运行。
在此例中我们可以看到,信号处理函数使用了一个单独的整数参数,它就是引起该函数被调用的信号代码。如果需要在同一个函数中处理多个信号,这个参数就很有用。我们打印出SIGINT的值,它的值在这个系统中是2,但你不能过分依赖传统的信号数字值,而应该在新的程序中总是使用信号的名字。
在信号处理函数中,调用如printf这样的函数是不安全的。一个有用的技巧是,在信号处理函数中设置一个标志,然后在主程序中检查该标志,如需要就打印一条消息。
该程序中安排函数ouch来处理在按下CTRL+C组合键所产生的SIGINT信号。程序会在中断函数ouch处理完毕后继续执行,但信号处理方式已恢复为默认行为。当它接收到第二个SIGINT信号后,程序将采取默认的行动,即终止程序的运行。
如果想保留信号处理函数,让它继续响应用户的CTRL+C组合键,我们就需要再次调用signal函数来重新建立它。这会使信号在一段时间内无法得到处理,这段时间从调用中断函数开始,到信号处理函数的重建为止。如果在这段时间内程序接收到第二个信号,它就会违背我们的意愿终止程序的运行。
我们不推荐使用signal接口。之所以介绍它,是因为可能会在许多老程序中看到它的应用。下面会介绍一个定义更清晰、执行更可靠的函数sigaction。
signal函数返回的是先前对指定信号进行处理的信号处理函数的函数指针,如果未定义信号处理函数,则返回SIG_ERR并设置errno为一个正数值。如果给出的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),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变量。失败的原因可能是:给定的信号无效(errno设置为EINVAL):发送进程权限不够(errno设置为EPERM);目标进程不存在(errno设置为ESRCH)。
信号向我们提供了一个有用的闹钟功能。进程可以通过调用alarm函数在经过预订时间后发送一个SIGALRM信号。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm函数用来在seconds秒之后安排发送一个SIGALRM信号。但由于处理的延时和时间调度不确定性,实际闹钟时间比预先安排的要稍微晚点。把参数seconds设置为0将取消所有已设置的闹钟请求。如果在接收到SIGALRM信号之前再次调用alarm函数,则闹钟重新开始计时。每个进程只能有一个闹钟时间。alarm函数的返回值是以前设置的闹钟时间的余留秒数,如果调用失败则返回-1。
为了说明alarm函数的工作情况,我们通过使用fork、sleep和signal来模拟它的效果。程序可以启动一个新的进程,它专门用于在未来的某一时刻发送一个信号。
实验:模拟一个闹钟
/* * alarm.c * * Created on: Aug 2, 2013 * Author: root */ #include <sys/types.h> #include <signal.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> static int alarm_fired=0; void ding(int sig) { alarm_fired=1; } /*在main函数中,我们告诉了子进程在等待5秒后发送一个SIGALRM信号给它的父进程 */ int main() { pid_t pid; printf("alarm application starting\n"); pid=fork(); switch(pid) { case -1: perror("fork failed"); exit(1); case 0: sleep(5); kill(getppid(),SIGALRM); exit(0); } /*父进程通过一个signal调用安排好捕获SIGALRM信号的工作,然后等待它的到来。*/ printf("waiting for alarm to go off\n"); (void)signal(SIGALRM,ding); pause(); if(alarm_fired) { printf("Ding\n"); } printf("done\n"); return 0; }
运行
[root@localhost C_test]# gcc -o alarm alarm.c -lm [root@localhost C_test]# ./alarm alarm application starting waiting for alarm to go off #等待5秒,等待闹钟的闹响 Ding done
这个程序用到了一个新的函数pause,它的作用很简单,就是把程序的执行挂起直到有一个信号出现为止。当程序接收到一个信号时,预设好的信号处理函数将开始运行,程序也将恢复正常的执行。
pause函数的定义如下:
#include <unistd.h>
int pause(void);
当它被一个信号中断时,将返回-1(如果下一个接收到的信号没有导致程序终止的话)并把errno设置为EINTR。当需要等待信号时,一个更常见的方法是使用稍后下面介绍的sigsuspend函数。
实验解析:
闹钟模拟程序通过fork调用启动新的进程。这个子进程休眠5秒后向其父进程发送一个SIGALRM信号。父进程在安排好捕获SIGALRM信号后暂停运行,直到接收到一个信号为止。我们并未在信号处理函数中直接调用printf,而是通过在该函数中设置标志,然后在main函数中检查该标志来完成消息的输出。
使用信号并挂起程序的执行是linux程序设计中的一个重要部分。这意味着程序不需要总是在执行着,程序不必在一个循环中无休止地检查某个事件是否已发生,相反,它可以等待事件的发生。这在只有一个CPU的多用户环境中尤为重要,进程共享着一个处理器,繁忙的等待将会对系统的性能造成极大的影响。程序中信号的使用将带来一个特殊的问题:如果信号出现在系统调用的执行过程中会发生什么情况?一般来说,只需要考虑慢系统调用,例如从终端读数据,如果在这个系统调用等待数据时出现一个信号,它就会返回一个错误。如果你开始在自己的程序中使用信号,就需要注意一些系统调用会因为接收到了一个信号而失败,而这种错误情况可能是你在添加信号处理函数之前没有考虑到的。
在编写程序中处理信号部分的代码时必须非常小心,因为在使用信号的程序中会出现各种各样的“竟态条件”。如想调用pause等待一个信号,可信号却出现在调用pause之前,就会使程序无限期得等待一个不会发生的事件。这些竟态条件都是一些对时间要求很苛刻的问题,许多编程新手都有这方面的烦恼,所以在检查和信号相关的代码时总是要非常小心。
一个健壮的信号接口
我们已经对signal和其他相关函数来生成和捕获信号做了比较深入的介绍,因为它们在传统的unix编程中很常见。但X/open和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函数设置与信号sig关联的动作。如果oact不是空指针,sigaction将把原先对该信号的动作写到它指向的位置。如果act是空指针,则sigaction函数就不需要再做其他设置了,否则将在该参数中设置对指定信号的动作。
与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。
实验:sigaction函数
/* * ctrlc2.c * * Created on: Aug 2, 2013 * Author: root */ #include <signal.h> #include <stdio.h> #include <unistd.h> void ouch(int sig) { printf("I got signal %d\n",sig); } int main(int argc, char **argv) { struct sigaction act; act.sa_handler=ouch; sigemptyset(&act.sa_mask); act.sa_flags=0; sigaction(SIGINT,&act,NULL); while(1) { printf("Hello World!\n"); sleep(1); } }
运行
[root@localhost C_test]# ./ctrlc2 Hello World Hello World Hello World ^CI got signal 2 Hello World Hello World ^CI got signal 2 Hello World ^\Quit (core dumped)
运行这个新版程序时,只要按下CTRL+C组合键,就可以看到一条消息。因为sigaction函数连续处理到来的SIGINT信号。要想终止这个程序,我们只能按下CTRL+\组合键,它在默认情况下产生SIGQUIT信号。运行完程序后,将发现在当前目录下多了一个core文件,你可以安全地删除它。
用sigaction代替signal来设置CTRL+C组合键的信号处理函数为ouch。它首先必须设置一个sigaction结构,在该结构中包含信号处理函数、信号屏蔽字和标志。本例中不需要设置任何标志,并通过调用新的函数sigemptyset来创建空的信号屏蔽字。
信号集
头文件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和sigdelset从信号集中增加或删除给定的信号。它们在成功时返回0,失败时返回-1并设置errno。只有一个错误代码被定义,即当给定的信号无效时,errno将设置为EINVAL。
函数sigismember判断一个给定的信号是否是一个信号集的成员。如果是就返回1;如果不是,它就返回0;如果给定的信号无效,它就返回-1并设置errno为EINVAL。
#include <signal.h>
int sigismember(sigset_t * set,int signo);
进程的信号屏蔽字的设置或检查工作由函数sigprocmask来完成。信号屏蔽字是指当前阻塞的一组信号,它们不能被当前进程接收到。
#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标志,那么在信号处理函数执行完之后,函数将被重启而不是被信号中断。
信号处理函数正在执行时,新接收到的信号将在该处理函数的执行期间被添加到进程的信号屏蔽字中。这防止了同一信号的不断出现引起信号处理函数的再次运行。如果信号处理函数是一个不可重入的函数,在它结束对第一个信号的处理之前又让另一个信号再次调用它就有可能引起问题。但如果设置了SA_NODEFER标志,当程序接收到这个信号时就不会改变信号屏蔽字。
信号处理函数可以在其执行期间被中断并再次被调用。当返回到第一次调用时,它能否继续正确操作是很关键的。这不仅仅是递归的问题,而是可重入(可以安全地进入和再次执行)的问题。linux内核中,在同一时间负责处理多个设备的中断服务例程就需要是可重入的,因为优先级更高的中断可能在同一段代码的执行期间“插入”进来。
下表列出的是可以在信号处理函数中安全调用的函数。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 |
所有未列在上表中的函数,在涉及信号处理时,都被认为是不安全的。
2 常用信号参考
这里列出linux和unix程序常用的信号及其默认行为。
下表中信号的默认动作都是异常终止进程,进程以_exit调用方式退出(它类似exit,但在返回到内核之前不做任何清理工作)。但进程的结束状态会传递到wait和waitpid函数中去,从而表明进程是因某个特定的信号而异常终止的。
信号名称 | 说明 |
SIGALRM | 由alarm函数设置的定时器产生 |
SIGHUP | 由一个处于非连接状态的终端发送给控制进程,或是由控制进程在自身结束时发送给每个前台进程 |
SIGINT | 一般由从终端敲入的CTRL+C组合键或预先设置好的中断字符产生。 |
SIGKILL | 因为这个信号不能被捕获或忽略,所以一般在shell中用它来强制终止异常进程。 |
SIGPIPE | 如果在向管道写数据时没有与之对应的读进程,就会产生这个信号 |
SIGTERM | 作为一个请求被发送,要求进程结束运行。unix在关机时用这个信号要求系统服务停止运行。它是kill命令默认发送的信号。 |
SIGUSR1,SIGUSR2 | 进程之间可以用这个信号进程通信,例如让进程报告状态信息等。 |
默认情况下,下表的信号也会引起进程的异常终止。但可能还会有一些与具体实现相关的其他动作,比如创建core文件等。
信号名称 | 说明 |
SIGFPE | 由浮点运算异常产生 |
SIGILL | 处理器执行了一条非法的指令。这通常是由一个崩溃的程序或无效的共享内存模块引起的 |
SIGQUIT | 一般从终端敲入的CTRL+\组合键或预先设置好的退出字符产生 |
SIGSEGV | 段冲突。一般是因为对内存中的无效地址进行读写而引起的,例如超越数组边界或引用无效指针。当函数返回到一个非法地址时,覆盖局部数组变量和引起栈崩溃都会引发SIGSEGV信号。 |
默认情况下,进程接收到下表中的信号时将会被挂起。
信号名称 | 说明 |
SIGSTOP | 停止执行(不能被捕获或忽略) |
SIGTSTP | 终端挂起信号。通常因按下CTRL+Z组合键而产生 |
SIGTTIN、SIGTTOU | shell用这两个信号表明后台作业因需要从终端读取输入或产生输出而暂停运行 |
SIGCONT信号的作用是重启被暂停的进程,如果进程没有暂停,则忽略该信号。SIGCHLD信号在默认情况下被忽略。
信号名称 | 说明 |
SIGCONT | 如果进程被暂停,就继续执行 |
SIGCHLD | 子进程暂停或退出时产生 |