一、UNIX进程环境
在学习UNIX进程工作原理时,我们应该先了解一下UNIX进程的基本环境是怎么样的,首先从main函数开始。
1、main函数
int main(int argc, char *argv[]);
相信main函数是我们非常熟悉的一个函数,它是C程序执行的入口函数。其中,argc是命令行参数的数目,agrv是指向参数的各个指针所构成的数组,而ISO/C和POSIX.1都要求argv[argc]是一个空指针。
当内核使用一个exec函数执行C程序时,在调用main函数前先调用一个特殊的启动例程,可执行程序文件将此启动例程指定为程序的起始地址,启动例程从内核取得命令行参数和环境变量值,为调用main函数做好准备。启动例程常常用汇编语言编写,从main函数返回后立即调用exit函数,如果以C代码表示,形式如下:
exit(main(argc, argv));
2、进程终止方式
进程正常终止方式有5种:
(1)从main返回。
(2)调用exit。先执行一些清理处理,包括调用执行各终止处理程序,关闭所有标准I/O流等,然后进入内核。
按照ISO/C的规定,一个进程可以登记多达32个函数,这些函数将由exit自动调用,我们称这些函数为终止处理程序,并调用atexit函数来登记这些函数。
#include <stdlib.h>
int atexit(void (*function)(void));
atexit函数的参数是一个函数指针,这个函数没有参数也没有返回值。exit函数调用这些函数的顺序与它们登记的顺序相反,同一函数如若登记多次,则也会被调用多次。
(3)调用_exit或_Exit。立即进入内核。
上面提到的三个exit函数,都带有一个整型参数,称之为终止状态或退出状态。如果调用这些函数时不带终止状态,或main函数执行了一个无返回值的return语句,或main没有声明返回类型为整型,则该进程的终止状态是未定义的。但是,若main函数的返回类型是整型,并且main函数执行到最后一条语句时返回,可能是隐式返回,那么该进程的终止状态是0。
(4)最后一个线程从其启动例程返回。
(5)最后一个线程调用pthread_exit。
进程异常终止方式有3种:
(1)调用abort。
(2)接到一个信号并终止。
(3)最后一个线程对取消请求做出响应。
3、环境表及环境变量
每个程序都会收到一张环境表,与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局变量environ则包含了该指针数组的地址:
extern char **environ;
按照惯例,环境变量由name=value这样的字符串组成。在历史上,大多数UNIX系统支持main函数带有三个参数,其中第三个参数就是环境表的地址:
int main(int argc, char *argv[], char *envp[]);
因为ISO/C规定main函数只有两个参数,而且第三个参数与全局变量environ相比也没有带来更多益处,所以POSIX.1也规定应使用environ而不使用第三个参数。通常用getenv和putenv函数来访问特定的环境变量,而不是用environ变量,但是,如果要查看整个环境,则必须使用environ指针。
#include <stdlib.h>
char *getenv(const char *name);
int putenv(char *string);
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);
getenv取环境变量值。putenv参数形式为“name=value”,如果name存在,先删除原来的定义然后更新value。setenv设置name为value,如果name存在,overwrite非0时先删除原来的定义然后更新value,overwrite为0时不做任何事情。这些函数在修改环境表时值得考究,因为环境表和环境字符串通常占用的是进程地址空间的顶部,所以它不能再向高地址方向扩展,同时也不能移动在它之下的各栈帧,所以它也不能向低地址方向扩展。
4、存储空间布局
C程序的存储空间布局由下面几部分组成:
(1)正文段。这是由CPU执行的机器指令部分,通常,正文段是可共享的,所以即使是频繁执行的程序在存储器中也只需有一个副本,另外,正文段是只读的,以防止程序由于意外而修改其自身的指令。
(2)初始化数据段或者叫数据段。数据段包含了程序中需明确地赋初值的变量,如C程序中出现在任何函数之外的声明。
(3)非初始化数据段或者叫bss段。在程序开始执行之前,内核将此段中的数据初始化为0或空指针。
(4)堆。通常在堆中进行动态存储分配。
(5)栈。自动变量以及每次函数调用时所需保存的信息都存放在此段上。
另外,可执行文件中还有若干其它类型的段,例如,包含符号表的段、包含调试信息的段以及包含动态共享库链接表的段等等,这些部分并不装载到进程执行的程序映像中。需要存放在程序文件中的段只有正文段和数据段,bss段的内容并不存放在磁盘上的程序文件中。
5、共享库
共享库可以减少每个可执行文件的长度,但增加了一些运行时间开销,这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时。共享库的另一个优点是可以用库函数的新版本代替老版本,而无需对使用该库的程序重新编译链接。
6、动态存储分配
ISO/C说明了三个用于存储空间动态分配的函数:
#include <stdlib.h>
void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
malloc分配指定字节数的存储区,此存储区中的初始值不确定。
calloc为指定数量具有指定长度的对象分配存储空间,该空间中的每一位都初始化为0。
realloc更改以前分配区的长度,增加或减少,当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区域内的初始值则不确定。
free函数释放ptr指向的存储空间,被释放的空间通常被送入可用存储区池,以后可在调用上述三个分配函数时再分配。
malloc、calloc、realloc这三个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对象。这些分配例程通常调用sbrk系统调用实现,该系统调用扩充或缩小进程的堆,但是大多数malloc和free的实现都不减小进程的存储空间,释放的空间可供以后再分配,通常将它们保持在malloc池中而不返回给内核。
有一点需要注意的是,大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息,如分配块的长度、指向下一个分配块的指针等等。这就意味着如果超过一个已分配区的尾端进行写操作,则会重写后一个块的管理记录,这种类型的错误是灾难行的,但是因为这种错误不会很快暴露出来,所以也就很难发现,同样在已分配区起始位置之前进行写操作会重写本块的管理记录。
如若一个进程调用malloc函数,但却忘记调用free函数,那么进程地址空间长度就会慢慢增加,造成内存泄漏,分页开销过度,进而使性能下降。如果释放一个已经释放了的块,或者调用free时所用的指针不是三个alloc函数的返回值也将产生致命性的错误。
7、栈帧跳转
在C中,goto语句是不能跨越函数的,而执行这类跳转功能的函数是setjmp和longjmp,这两个函数对于处理发生在深层嵌套函数调用中的出错情况是非常有用的。
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
先在某个地方setjmp后,然后在另一个地方调用longjmp,栈帧会跳转到setjmp的地方,这里有一个问题需要注意一下,就是栈帧跳转之后,变量能否回滚到以前调用setjmp时的值,答案是不确定的。以自动变量(函数内变量默认类型)、register变量、volatile变量、static变量为例,如果GCC编译不带任何优化选项,所有类型的变量是不会回滚的,如果GCC编译带优化选项,如“-O”选项,自动变量和寄存器变量回滚,其它类型的变量不回滚。换句话说,存放在存储器中的变量将具有longjmp时的值而不回滚,而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值,不进行优化时,所有类型的变量都存放在存储器中,而进行了优化后,自动变量和register变量则存放在寄存器中,其它变量仍存放在存储器中。关于自动变量,当声明自动变量的函数已经返回后,将不能再引用这些自动变量,想要使用这些变量时,应在全局存储空间静态地(如static或extern)或者动态地(使用一种alloc函数)为变量分配空间。
8、资源限制
每个进程都有一组资源限制,可以使用如下函数查询和修改:
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
struct rlimit {
rlim_t rlim_cur; /* Soft limit */
rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
进程的资源限制通常是在系统初始化时由进程0建立的,然后由每个后续进程继承,每种实现都可以用自己的方法对各种限制作出调整。在更改资源限制时,有下列三条规则:
(1)任何一个进程都可将一个软限制值更改为小于或等于其硬限制值。
(2)任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值,这种降低对普通用户而言是不可逆的。
(3)只有超级用户进程可以提高硬限制值。
二、UNIX进程控制
说起UNIX进程,其身份标识符便是我们熟悉的PID,即进程ID,这个ID是唯一的,但却是可以重用的,当一个进程终止后,其进程ID就可以再次被使用了了。正因为如此,大多数UNIX系统实现延迟重用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID,这防止了将新进程误认为是使用同一ID的某个已终止的先前进程。
系统中有一些专用的进程,但具体细节因实现而异。ID为0的进程通常是调度进程,常常被称为交换进程swapper,该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被成为系统进程。ID为1的进程通常是init进程,在自举过程结束时由内核调用,启动一个UNIX系统,通常读取与系统有关的初始化文件,并将系统引导到一个状态,init进程不会终止,它虽是一个普通的用户进程,但以超级用户特权运行,是所有孤儿进程的父进程。
除了进程ID之外,每个进程还有一些其它的标识符,如父进程ID、实际与有效用户ID、实际与有效组ID,它们可以通过下列函数获取:
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);
1、fork
#include <unistd.h>
pid_t fork(void);
一个现有进程可以调用fork函数创建一个新进程,新进程即子进程,子进程中返回0(一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID),父进程中返回子进程ID(因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID),出错返回-1,需要注意的是调用fork成功时返回两次。在很多场合下fork之后跟随一个exec函数,某些操作系统将这两个操作组合成一个接口spawn。
子进程和父进程继续执行fork调用之后的指令。子进程是父进程发副本,例如,子进程获得父进程数据空间、堆和栈的副本,注意,这是子进程所拥有的副本,父、子进程并不共享这些存储空间部分。父、子进程共享正文段。
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。如果要求父、子进程之间相互同步,则要求某种形式的进程间通信。当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为发生了竞争条件,哪一个进程先运行是无法预料的。在子进程等待父进程终止时,有时候我们会用一个循环方式来等待测试,判断其父进程ID是否是init进程ID,但这又浪费了CPU时间。通常的做法是使用UNIX的信号机制或者IPC通信。
关于文件共享,在fork之后处理文件描述符有两种常见的情况,一种是父进程等待子进程,另一种是父、子进程各自执行不同的程序段,否则会发生冲突。
2、vfork
#include <unistd.h>
pid_t vfork(void);
vfork函数的调用序列和返回值与fork相同,但两者的语义不同。vfork用于创建一个新进程,而该新进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程立即调用exec或exit,于是也就不会存放该地址空间。相反,在子进程调用exec或exit之前,它在父进程的空间中运行。vfork和fork之间的另一个区别是vfork保证子进程先运行,在它调用exec或exit只有父进程才可能被调度运行,如果在调用这两个函数之前子进程依赖与父进程的进一步动作,则会导致死锁。
3、exit
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
前面描述了进程终止的几种方式,但不管进程如何终止,最后都会执行内核中的同一段代码,这段代码为相应进程关闭所有打开的描述符,释放它所使用的存储器等。子进程终止时,终止状态要通知到其父进程,该终止进程的父进程可以用wait或waitpid函数取得其终止状态。
但是,如果父进程在子进程之前终止,则将如何呢?对于父进程提前终止的所有子进程,它们的父进程都将改变为init进程,由init进程领养。
在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理的进程被称为僵尸进程。如果编写一个长期运行的程序,它调用fork产生了很多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程终止后就会变成僵尸进程。
4、wait
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
当一个进程正常终止或异常终止时,内核就向其父进程发送SIGCHILD信号,因为子进程是个异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以忽略该信号,或者提供一个该信号发生时的信号处理程序,对于这种信号的系统默认动作是忽略它。
wait等待子进程终止,会阻塞。waitpid等待指定的子进程,其参数可控制阻塞与否。检查wait和waitpid所返回的终止状态可使用四个互斥的宏,这些宏以WIF开头,如WIFEXITED。waitid类似于waitpid,但提供了更多的灵活性,它使用单独的参数表示要等待的子进程的类型,而不是将此与进程ID或进程组ID组合成一个参数。wait3和wait4则多提供了另一个功能,rusage参数要求内核返回由终止进程及其所有子进程使用的资源汇总,如用户CPU时间总量、系统CPU时间总量、页面出错次数、接收到信号的次数等。
5、exec
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变,exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。在很多UNIX实现中,这6个函数中只有execve是内核的系统调用,另外5个只是库函数,它们最终都要调用该系统调用。
6、用户ID与组ID
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
int seteuid(uid_t euid);
int setegid(gid_t egid);
在UNIX系统中,特权是基于用户ID和组ID的。当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们需要更换自己的用户ID或组ID,使得新ID具有合适的特权或访问权限。与此类似,当程序需要降低其特权或阻止对某些资源访问时,也需要更换用户ID或组ID,从而使新ID不具有相应特权或访问这些资源的能力。
一般而言,在设计应用程序时,我们总是试图使用最小特权模型。依照此模型,我们的程序应当只具有为完成给定任务所需的最小特权。这减少了安全性受到损害的可能性,这种安全性损害是由恶意用户试图哄骗我们的程序以未预料的方式使用特权所造成的。
下面以用户ID为例,组ID用法类似。
setuid函数可以设置实际用户ID和有效用户ID。若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID,以及保存的设置用户ID设置为uid;若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户ID,则setuid只将有效用户ID设置为uid,不改变实际用户ID和保存的设置用户ID;如果前面两个条件都不满足,则将errno设置为EPERM,并返回-1。保存的设置用户ID有效的前提是_POSIX_SAVED_IDS为真。
关于内核所维护的三个用户ID,还要注意下列几点:
(1)只有超级用户进程可以更改实际用户ID,通常实际用户ID是在用户登录时,由login程序设置的,而且永远不会改变它,因为login是一个超级用户进程,当它调用setuid时,会设置所有三个用户ID。
(2)仅当对程序文件设置了设置用户ID位时,exec函数才会设置有效用户ID,如果设置用户ID位没有设置,则exec函数不会改变有效用户ID,而将其维持为原先值,任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户ID,自然不能将有效用户ID设置为任意随机值。
(3)保存的设置用户ID是由exec复制有效用户ID而得来的,如果设置了文件的设置用户ID位,则在exec根据文件的用户ID设置了进程的有效用户ID以后,就将这个副本保存起来。
setreuid可以交换实际用户ID和有效用户ID,seteuid只更改有效用户ID。
任一进程都可以得到其实际和有效用户ID及组ID,但是有时希望找到运行该程序的用户登录名。我们可以调用getpwuid(getuid()),但是如果一个用户有多个登录,这些登录名又对应着同一个用户ID,那么又将如何呢?这时,可以调用getlogin函数获取用户登录名,然后用返回值作为参数调用getpwnam函数即可。
7、解释器文件
所有现今的UNIX系统都支持解释器文件,这种文件是文本文件,其起始行的形式是:
#! pathname [optional-argument]
感叹号和pathname之间的空格是可选的,最常见的python脚本启动行格式如下:
#!/usr/bin/env python
pathname通常是绝对路径名,对它不进行什么特殊的处理,即不使用PATH进行路径搜索。对这种文件的识别是由内核作为exec系统调用处理的一部分来完成的,内核使调用exec函数的进程实际执行的并不是该解释器文件,而是解释器文件第一行pathname所指定的文件。
8、system
#include <stdlib.h>
int system(const char *command);
在程序中执行一个命令字符串很方便,调用system函数即可,其实现要调用fork、exec、waitpid,下面例子是system的一种实现,它对信号没有进行处理。
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
int system(const char *command)
{
pid_t pid;
int status;
if (command == NULL) {
return(1);
}
if ((pid = fork()) < 0) {
status = -1;
}
else if (0 == pid) {
execl(‘‘/bin/sh‘‘, ‘‘sh‘‘, ‘‘-c‘‘, command, (char*)0);
_exit(127);
}
else {
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
status = -1;
break;
}
}
}
return status;
}
9、进程会计
#include <unistd.h>
int acct(const char *filename);
大多数UNIX系统提供了一个选项以进行进程会计处理,启用该选项后,每当进程结束时内核就写一个会计记录。典型的会计记录包含较小的二进制数据,一般包括命令名、所使用的CPU时间总量、用户ID和组ID、启动时间等。acct可以把会计记录写到指定文件filename中,filename为空指针时关闭会计记录。会计记录定义在sys/acct.h中,结构如下:
#define ACCT_COMM 16
/*
comp_t is a 16-bit "floating" point number with a 3-bit base 8
exponent and a 13-bit fraction. See linux/kernel/acct.c for the
specific encoding system used.
*/
typedef u_int16_t comp_t;
struct acct
{
char ac_flag; /* Flags. */
u_int16_t ac_uid; /* Real user ID. */
u_int16_t ac_gid; /* Real group ID. */
u_int16_t ac_tty; /* Controlling terminal. */
u_int32_t ac_btime; /* Beginning time. */
comp_t ac_utime; /* User time. */
comp_t ac_stime; /* System time. */
comp_t ac_etime; /* Elapsed time. */
comp_t ac_mem; /* Average memory usage. */
comp_t ac_io; /* Chars transferred. */
comp_t ac_rw; /* Blocks read or written. */
comp_t ac_minflt; /* Minor pagefaults. */
comp_t ac_majflt; /* Major pagefaults. */
comp_t ac_swaps; /* Number of swaps. */
u_int32_t ac_exitcode; /* Process exitcode. */
char ac_comm[ACCT_COMM+1]; /* Command name. */
char ac_pad[10]; /* Padding bytes. */
};
10、进程时间
#include <sys/times.h>
clock_t times(struct tms *buf);
struct tms {
clock_t tms_utime; /* user time */
clock_t tms_stime; /* system time */
clock_t tms_cutime; /* user time of children */
clock_t tms_cstime; /* system time of children */
};
进程时间包括墙上时钟时间、用户CPU时间和系统CPU时间,任一进程都可以调用times函数以获得这些时间及已终止子进程的时间。tms结构没有包含墙上时钟时间,可以用times函数的返回值求取,返回值是个相对时间,墙上时钟时间可使用两次调用times函数的返回值之差来计算。
三、UNIX进程关系
1、进程组与会话
每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合,通常它们与同一作业相关联,可以接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID,进程组ID类似与进程ID,是一个正整数,并可存放在pid_t数据类型中。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生存期,进程组中的最后一个进程可以终止,或者转移到另一个进程组,且一个进程只能为它自己或它自己的子进程设置进程组ID。
每个进程组都可以有一个组长进程,组长进程的标识是其进程组ID等于其进程ID。组长进程可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。
会话是一个或多个进程组的集合,相关的操作函数是setsid和getsid。
2、控制终端
会话和进程组特性:
(1)一个会话可以有一个控制终端,这通常是登录到其上的终端设备或伪终端。
(2)建立与控制终端连接的会话首进程被称为控制进程。
(3)一个会话中的几个进程组可被分成一个前台进程组以及一个或多个后台进程组。
(4)如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其它进程组则为后台进程组。
(5)无论何时键入终端的中断键,就会将中断信号发送给前台进程组的所有进程。
(6)无论何时键入终端的退出键,就会将退出信号发送给前台进程组中的所有进程。
(7)如果终端接口检测到调制解调器或网络已经断开链接,则将挂断信号发送给控制进程。
通常,我们不必关心控制终端,登录时将自动建立控制终端。如果想知道哪一个进程组是前台进程组,可通过tcgetpgrp函数获取。
3、作业控制
作业控制允许一个终端上启动多个作业,即多个进程组,并控制哪一个作业可以访问该终端,以及哪些作业在后台运行。从shell使用作业控制功能角度讲,用户可以在前台或后台启动一个作业,一个作业只是几个进程的集合,通常是一个进程的管道线。当启动一个后台作业时,shell赋予它一个作业标识号码,并显示一个或几个进程ID。
4、孤儿进程组
前面提到了孤儿进程,一个进程的父进程提前终止时,这个进程将被init进程收养,同样,进程组也可能是一个孤儿进程组。POSIX.1将孤儿进程组定义为:该组中每个成员的父进程要么是该组的一个成员,要么不是该组所属会话的成员。对孤儿进程组的另一种描述是:一个进程组不是孤儿进程组的条件是,该进程组中有一个进程,其父进程在属于同一会话的另一个组中。如果进程组不是孤儿进程组,那么在属于同一会话的另一个组中的父进程就有机会重新启动该组中停止的进程。
四、信号
信号是软件中断,它提供了一种处理异步事件的方法。每个信号都有一个名字,这些名字都以三个字符SIG开头。在头文件signal.h中,这些信号都被宏定义为正整数,不存在编号为0的信号,这是因为kill函数对信号0有特殊的应用,POSIX.1将此种信号称为空信号。
1、信号产生的条件
很多条件可以产生信号,如下:
(1)当用户按某些终端键时,引发终端产生信号。例如,在终端上按Ctrl+C组合键,通常产生中断信号SIGINT,这是停止一个已失去控制程序的方法。
(2)硬件异常产生信号。例如,除数为0或者引用无效内存,这些条件通常由硬件检测到,并将其通知内核,然后内核为该条件发生时正在运行的进程产生适当的信号,如对执行一个无效内存引用的进程产生SIGSEGV信号。
(3)进程调用kill函数将信号发送给另一个进程或进程组。这种情况下,接收信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者必须是超级用户。
(4)用户用kill命令将信号发送给其它进程。常用此命令终止一个失控的后台进程。
(5)当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。
2、信号处理
信号处理一般有下列三种方法:
(1)忽略信号。SIGKILL和SIGSTOP信号是不能被忽略的。
(2)捕捉信号。在某种信号发生时,要通知内核调用一个用户函数,但不能捕捉SIGKILL和SIGSTOP信号。
(3)执行系统默认动作。大多数信号的系统默认动作是终止进程。
在早期的UNIX版本例如V7中,信号是不可靠的,也就是说信号可能会丢失。在信号产生和递送之间的时间间隔内,信号是pending未决的。
3、signal函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal函数是UNIX系统的信号机制中最简单的接口,其参数包含一个函数指针,返回值类型也是一个函数指针。
4、中断的系统调用
早期UNIX系统的一个特性是:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno被设置为EINTR。这样处理的理由是:因为一个信号发生了,进程捕捉到了它,这意味这已经发生了某种事情,所以是个应当唤醒阻塞的系统调用的好机会。为了支持这种特性,将系统调用分成两类:低速系统调用和其它系统调用,低速系统调用是可能会使进程永远阻塞的一类系统调用。为了帮助应用程序使其不必处理被中断的系统调用,4.2BSD引入了某些被中断系统调用的 自动重启动,包括ioctl、read、readv、write、writev、wait和waitpid。
5、可重入函数
什么是可重入函数?进程捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回,则继续执行在捕捉到信号时进程正在执行的正常指令序列。但在信号处理程序中,不能判断捕捉到信号时进程在何处执行,这时会发生什么;如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc,这时就可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一个链接表,而插入执行信号处理程序时,进程可能正在更改次链接表。所以,UNIX定义了一些可重入函数,保证不会出现上述问题。有些函数,如果使用了静态数据结构,调用malloc或free,或者是标准I/O函数,通常是不可重入函数。
6、几个函数
#include <signal.h>
int kill(pid_t pid, int sig);
int raise(int sig);
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);
int sigsuspend(const sigset_t *mask);
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
int pause(void);
unsigned int sleep(unsigned int seconds);
#include <stdlib.h>
void abort(void);
#include <setjmp.h>
int setjmp(jmp_buf env);
void siglongjmp(sigjmp_buf env, int val);
kill函数将信号发送给进程或进程组,raise函数则允许进程向自身发送信号。使用alarm函数可以设置一个计时器,在将来某个指定的时间该计时器会超时,当计时器超时时,产生SIGALRM信号,如果不忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程。pause函数使调用进程挂起直到捕捉到一个信号。sleep函数让进程挂起,休眠指定时间或者捕捉到信号并从信号处理程序中返回时结束。调用sigprocmask函数可以检测或更改进程的信号屏蔽字。sigpending函数通过set参数获得信号集。sigsuspend函数是sigprocmask和pause函数的集合,但sigsuspend是个原子操作。sigaction函数的功能是检查或修改与指定信号相关联的处理动作。abort函数的功能是使异常程序终止。sigsetjmp和siglongjmp函数在信号处理程序中进行非局部转移。
7、信号集
信号集及多个信号的集合,相关函数如下:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
end…
版权声明:本文为博主原创文章,未经博主允许不得转载。