赞
踩
信号概述
信号是 UNIX 中所使用的进程通信的一种最古老的方法。它是在软件层次上对中断机制的一种模拟,是一种异步通信方式。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;
如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。在第 2 章 kill 命令中曾讲解到“−l”选项,这个选项可以列出该系统所支持的所有信号的列表。在笔者的系统中,信号值在 32 之前的则有不同的名称,而信号值在 32 以后的都是用“SIGRTMIN”或“SIGRTMAX”开头的,这就是两类典型的信号。前者是从 UNIX 系统中继承下来的信号,为不可靠信号(也称为非实时信号);后者是为了解决前面“不可靠信号”的问题而进行了更改和扩充的信号,称为“可靠信号”(也称为实时信号)。那么为什么之前的信号不可靠呢?这里首先要介绍一下信号的生命周期。
一个完整的信号生命周期可以分为 3 个重要阶段,这 3 个阶段由 4 个重要事件来刻画的:信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数,如图 8.6 所示。相邻两个事件的时间间隔构成信号生命周期的一个阶段。要注意这里的信号处理有多种方式,一般是由内核完成的,当然也可以由用户进程来完成,故在此没有明确画出。
一个不可靠信号的处理过程是这样的:如果发现该信号已经在进程中注册,那么就忽略该信号。
因此,若前一个信号还未注销又产生了相同的信号就会产生信号丢失。而当可靠信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此信号就不会丢失。所有可靠信号都支持排队,而所有不可靠信号都不支持排队。
这里信号的产生、注册和注销等是指信号的内部实现机制,而不是调用信号的函数实现。因此,信号注册与否,与本节后面讲到的发送信号函数(如 kill()等)以及信号安装函数(如 signal()等)无关,只与信号值有关。
用户进程对信号的响应可以有 3 种方式。
忽略信号,即对信号不做任何处理,但是有两个信号不能忽略,即 SIGKILL 及 SIGSTOP。
捕捉信号,定义信号处理函数,当信号发生时,执行相应的自定义处理函数。
执行缺省操作,Linux 对每种信号都规定了默认操作。
Linux 中的大多数信号是提供给内核的。
信号发送与捕捉
发送信号的函数主要有 kill()、raise()、alarm()以及 pause(),下面就依次对其进行介绍。
1.kill()和 raise()
(1)函数说明。
kill()函数同读者熟知的 kill 系统命令一样,可以发送信号给进程或进程组(实际上,kill 系统命令只是 kill()函数的一个用户接口)。这里需要注意的是,它不仅可以中止进程(实际上发出 SIGKILL 信号),也可以向进程发送其他信号。
与 kill()函数所不同的是,raise()函数允许进程向自身发送信号。
下面这个示例首先使用 fork()创建了一个子进程,接着为了保证子进程不在父进程调用 kill()之前退出,在子进程中使用 raise()函数向自身发送 SIGSTOP 信号,使子进程暂停。接下来再在父进程中调用 kill()向子进程发送信号,在该示例中使用的是 SIGKILL,读者可以使用其他信号进行练习。
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid; int ret; /* 创建一子进程 */ if ((pid = fork()) < 0) { printf("Fork error\n"); exit(1); } if (pid == 0) { /* 在子进程中使用 raise()函数发出 SIGSTOP 信号,使子进程暂停 */ printf("Child(pid : %d) is waiting for any signal\n", getpid()); raise(SIGSTOP); exit(0); } else { /* 先执行父进程的话,子进程就没办法执行到*/ sleep(1); /* 在父进程中收集子进程发出的信号,并调用 kill()函数进行相应的操作,WNOHANG:不阻塞运行 */ if ((waitpid(pid, NULL, WNOHANG)) == 0) { if ((ret = kill(pid, SIGKILL)) == 0) { printf("Parent kill %d\n",pid); } } waitpid(pid, NULL, 0); exit(0); } }
alarm()和 pause()
(1)函数说明。
alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送SIGALARM 信号。要注意的是,一个进程只能有一个闹钟时间,如果在调用 alarm()之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。
pause()函数是用于将调用进程挂起直至捕捉到信号为止。这个函数很常用,通常可以用于判断信号是否已到。
该实例实际上已完成了一个简单的 sleep()函数的功能,由于 SIGALARM 默认的系统动作为终止该进程,因此程序在打印信息之前,就会被结束了。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
/*调用 alarm 定时器函数*/
int ret = alarm(5);
pause();
printf("I have been waken up.\n",ret); /* 此语句不会被执行 */
}
信号的处理
在了解了信号的产生与捕获之后,接下来就要对信号进行具体的操作了。从前面的信号概述中读者也可以看到,特定的信号是与一定的进程相联系的。也就是说,一个进程可以决定在该进程中需要对哪些信号进行什么样的处理。例如,一个进程可以选择忽略某些信号而只处理其他一些信号,另外,一个进程还可以选择如何处理信号。总之,这些都是与特定的进程相联系的。因此,首先就要建立进程与其信号之间的对应关系,这就是信号的处理。
注意:请读者注意信号的注册与信号的处理之间的区别,前者信号是主动方,而后者进程
是主动方。信号的注册是在进程选择了特定信号处理之后特定信号的主动行为。
信号处理的主要方法有两种,一种是使用简单的 signal()函数,另一种是使用信号集函数组。下面分别介绍这两种处理方式。
1.信号处理函数
(1)函数说明。
使用 signal()函数处理时,只需要指出要处理的信号和处理函数即可。它主要是用于前 32 种非实时信号的处理,不支持信号传递信息,但是由于使用简单、易于理解,因此也受到很多程序员的欢迎。Linux 还支持一个更健壮、更新的信号处理函数 sigaction(),推荐使用该函数。
该函数原型整体指向一个无返回值并且带一个整型参数的函数指针,也就是信号的原始配置函
数。接着该原型又带有两个参数,其中的第二个参数可以是用户自定义的信号处理函数的函数指针。
这里要说明的是 sigaction()函数中第 2 个和第 3 个参数用到的 sigaction 结构。
首先给出了 sigaction 的定义,如下所示:
struct sigaction
{
void (*sa_handler)(int signo);
sigset_t sa_mask;
int sa_flags;
void (*sa_restore)(void);
}
sa_handler 是一个函数指针,指定信号处理函数,这里除可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式)或 SIG_IGN(忽略信号)。它的处理函数只有一个参数,即信号值。sa_mask 是一个信号集,它可以指定在信号处理程序执行过程中哪些信号应当被屏蔽,在调用信号捕获函数之前,该信号集要加入到信号的信号屏蔽字中。
sa_flags 中包含了许多标志位,是对信号进行处理的各个选择项。
第一个实例表明了如何使用 signal()函数捕捉相应信号,并做出给定的处理。这里,my_func 就是信号处理的函数指针。读者还可以将其改为 SIG_IGN 或 SIG_DFL 查看运行结果。第二个实例是用 sigaction()函数实现同样的功能。
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> /*自定义信号处理函数*/ void my_func(int sign_no) { if (sign_no == SIGINT) { printf("I have get SIGINT\n"); } else if (sign_no == SIGQUIT) { printf("I have get SIGQUIT\n"); } } int main() { printf("Waiting for signal SIGINT or SIGQUIT...\n"); /* 发出相应的信号,并跳转到信号处理函数处 */ signal(SIGINT, my_func); signal(SIGQUIT, my_func); pause(); exit(0); }
/自定义信号处理函数/
void my_func(int sign_no)
{
if (sign_no == SIGINT)
{
printf(“I have get SIGINT\n”);
}
else if (sign_no == SIGQUIT)
{
printf(“I have get SIGQUIT\n”);
}
}
int main()
{
struct sigaction action;
printf(“Waiting for signal SIGINT or SIGQUIT…\n”);
/* sigaction 结构初始化 */
action.sa_handler = my_func;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
/* 发出相应的信号,并跳转到信号处理函数处 */
sigaction(SIGINT, &action, 0);
sigaction(SIGQUIT, &action, 0);
pause();
exit(0);
}
信号集函数组
(1)函数说明。
使用信号集函数组处理信号时涉及一系列的函数,这些函数按照调用的先后次序可分为以下几大功能模
块:创建信号集合、注册信号处理函数以及检测信号。
其中,创建信号集合主要用于处理用户感兴趣的一些信号,其函数包括以下几个。
sigemptyset():将信号集合初始化为空。
sigfillset():将信号集合初始化为包含所有已定义的信号的集合。
sigaddset():将指定信号加入到信号集合中去。
sigdelset():将指定信号从信号集合中删除。
sigismember():查询指定信号是否在信号集合之中。
注册信号处理函数主要用于决定进程如何处理信号。这里要注意的是,信号集里的信号并不是真正可以处理的信号,只有当信号的状态处于非阻塞状态时才会真正起作用。因此,首先使用 sigprocmask()函数检测并更改信号屏蔽字(信号屏蔽字是用来指定当前被阻塞的一组信号,它们不会被进程接收),然后使用 sigaction()函数来定义进程接收到特定信号之后的行为。
检测信号是信号处理的后续步骤,因为被阻塞的信号不会传递给进程,所以这些信号就处于“未处理”状态(也就是进程不清楚它的存在)。sigpending()函数允许进程检测“未处理”信号,并进一步决定对它们作何处理。
此处,若 set 是一个非空指针,则参数 how 表示函数的操作方式;若 how 为空,则表示忽略此操作。
总之,在处理信号时,一般遵循如图 8.7 所示的操作流程。
该实例首先把 SIGQUIT、SIGINT 两个信号加入信号集,然后将该信号集合设为阻塞状态,并进入用户输入状态。用户只需按任意键,就可以立刻将信号集合设置为非阻塞状态,再对这两个信号分别操作,其中SIGQUIT 执行默认操作,而 SIGINT 执行用户自定义函数的操作。
#include <sys/types.h> #include <unistd.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> /*自定义的信号处理函数*/ void my_func(int signum) { printf("If you want to quit,please try SIGQUIT\n"); } int main() { sigset_t set,pendset; struct sigaction action1,action2; /* 初始化信号集为空 */ if (sigemptyset(&set) < 0) { perror("sigemptyset"); exit(1); } /* 将相应的信号加入信号集 */ if (sigaddset(&set, SIGQUIT) < 0) { perror("sigaddset"); exit(1); } if (sigaddset(&set, SIGINT) < 0) { perror("sigaddset"); exit(1); } if (sigismember(&set, SIGINT)) { sigemptyset(&action1.sa_mask); action1.sa_handler = my_func; action1.sa_flags = 0; sigaction(SIGINT, &action1, NULL); } if (sigismember(&set, SIGQUIT)) { sigemptyset(&action2.sa_mask); action2.sa_handler = SIG_DFL; action2.sa_flags = 0; sigaction(SIGQUIT, &action2,NULL); } /* 设置信号集屏蔽字,此时 set 中的信号不会被传递给进程,暂时进入待处理状态 */ if (sigprocmask(SIG_BLOCK, &set, NULL) < 0) { perror("sigprocmask"); exit(1); } else { printf("Signal set was blocked, Press any key!"); getchar(); } /* 在信号屏蔽字中删除 set 中的信号 */ if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) { perror("sigprocmask"); exit(1); } else { printf("Signal set is in unblock state\n"); } while(1); exit(0); }
该程序的运行结果如下所示,可以看见,在信号处于阻塞状态时,所发出的信号对进程不起作用,并且该信号进入待处理状态。读者输入任意键,并且信号脱离了阻塞状态之后,用户发出的信号才能正常运行。这里 SIGINT 已按照用户自定义的函数运行,请读者注意阻塞状态下 SIGINT 的处理和非阻塞状态下SIGINT 的处理有何不同。
信号量概述在多任务操作系统环境下,多个进程会同时运行,并且一些进程之间可能存在一定的关联。多个进程可能为了完成同一个任务会相互协作,这样形成进程之间的同步关系。而且在不同进程之间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程之间的互斥关系。进程之间的互斥与同步关系存在的根源在于临界资源。临界资源是在同一个时刻只允许有限个(通常只有一个)进程可以访问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器以及其他外围设备等)和软件资源(共享代码段,共享结构和变量等)。访问临界资源的代码叫做临界区,临界区本身也会成为临界资源。
信号量是用来解决进程之间的同步与互斥问题的一种进程之间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(PV 操作)。其中信号量对应于某一种资源,取一个非负的整型值。信号量值指的是当前可用的该资源的数量,若它等于 0 则意味着目前没有可用的资源。
PV 原子操作的具体定义如下:
P 操作:如果有可用的资源(信号量值>0),则占用一个资源(给信号量值减去一,进入临界区代码);如果没有可用的资源(信号量值等于 0),则被阻塞到,直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。
V 操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程。如果没有进程等待它,则释放一个资源(给信号量值加一)。
使用信号量访问临界区的伪代码所下所示:
{
/* 设 R 为某种资源,S 为资源 R 的信号量*/
INIT_VAL(S); /* 对信号量 S 进行初始化 */
非临界区;
P(S); /* 进行 P 操作 */
临界区(使用资源 R); /* 只有有限个(通常只有一个)进程被允许进入该区*/
V(S); /* 进行 V 操作 */
非临界区;
}
最简单的信号量是只能取 0 和 1 两种值,这种信号量被叫做二维信号量。在本节中,主要讨论二维信号量。二维信号量的应用比较容易地扩展到使用多维信号量的情况。
函数说明
在 Linux 系统中,使用信号量通常分为以下几个步骤。
(1)创建信号量或获得在系统已存在的信号量,此时需要调用 semget()函数。不同进程通过使用同一个信号量键值来获得同一个信号量。
(2)初始化信号量,此时使用 semctl()函数的 SETVAL 操作。当使用二维信号量时,通常将信号量初始化为 1。
(3)进行信号量的 PV 操作,此时调用 semop()函数。这一步是实现进程之间的同步和互斥的核心工作部分。
(4)如果不需要信号量,则从系统中删除它,此时使用 semclt()函数的 IPC_RMID 操作。此时需要注意,在程序中不应该出现对已经被删除的信号量的操作。
本实例说明信号量的概念以及基本用法。在实例程序中,首先创建一个子进程,接下来使用信号量来控制两个进程(父子进程)之间的执行顺序。
因为信号量相关的函数调用接口比较复杂,我们可以将它们封装成二维单个信号量的几个基本函数。它们分别为信号量初始化函数(或者信号量赋值函数)init_sem()、P 操作函数 sem_p()、V 操作函数 sem_v()以及删除信号量的函数 del_sem()等。
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/ipc.h> #include <sys/sem.h> #define DELAY_TIME 3 /* 为了突出演示效果,等待几秒钟,*/ union semun { int val; struct semid_ds *buf; unsigned short *array; }; int init_sem(int sem_id, int init_value) { union semun sem_union; sem_union.val = init_value; /* init_value 为初始值 */ if (semctl(sem_id, 0, SETVAL, sem_union) == -1) { perror("Initialize semaphore"); return -1; } return 0; } /* 从系统中删除信号量的函数 */ int del_sem(int sem_id) { union semun sem_union; if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) { perror("Delete semaphore"); return -1; } } /* P 操作函数 */ int sem_p(int sem_id) { struct sembuf sem_b; sem_b.sem_num = 0; /* 单个信号量的编号应该为 0 */ sem_b.sem_op = -1; /* 表示 P 操作 */ sem_b.sem_flg = SEM_UNDO; /* 系统自动释放将会在系统中残留的信号量*/ if (semop(sem_id, &sem_b, 1) == -1) { perror("P operation"); return -1; } return 0; } /* V 操作函数*/ int sem_v(int sem_id) { struct sembuf sem_b; sem_b.sem_num = 0; /* 单个信号量的编号应该为 0 */ sem_b.sem_op = 1; /* 表示 V 操作 */ sem_b.sem_flg = SEM_UNDO; /* 系统自动释放将会在系统中残留的信号量*/ if (semop(sem_id, &sem_b, 1) == -1) { perror("V operation"); return -1; } return 0; } int main(void) { pid_t result; int sem_id; sem_id = semget(ftok(".", 'a'), 1, 0666|IPC_CREAT); /* 创建一个信号量*/ init_sem(sem_id, 0); /*调用 fork()函数*/ result = fork(); if(result == -1) { perror("Fork\n"); } else if (result == 0) /*返回值为 0 代表子进程*/ { printf("Child process will wait for some seconds...\n"); sleep(DELAY_TIME); printf("The returned value is %d in the child process(PID = %d)\n", result, getpid()); sem_v(sem_id); } else /*返回值大于 0 代表父进程*/ { sem_p(sem_id); printf("The returned value is %d in the father process(PID = %d)\n", result, getpid()); sem_v(sem_id); del_sem(sem_id); } exit(0); }
信号量怎么解决多进程之间存在的同步问题。我们将在后面讲述的共享内存和消息队列的
实例中,看到使用信号量实现多进程之间的互斥。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。