1、信号基础
信号是提供异步处理机制的软件中断
信号的名称与编号
- 信号是很短的消息,本质就是一个整数,用以区分代表不同事件的不同信号
- 通过kill -l 命令可以查看信号
- 一共有62个信号,其中前31个信为不可靠的非实时信号,后31个为可靠的实时信号
常用信号
2、信号处理
- 忽略:什么也不做,但是SIGKILL(9)和SIGSTOP(19)不能被忽略
- 默认:在没有人为设置的情况,系统缺省的处理行为。
- 捕获:接收到信号的进程会暂停执行,转而执行一段事先编写好的处理代码,执行完毕后再从暂停执行的地方继续运行。
// 头文件 signal.h
typedef void (* sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
- 功能:设置调用进程针对特定信号的处理方式
- 参数:
- signum信号编号
- handler信号的处理方式,可以如下取值
SIG_IGN - 忽略
SIG_DFL - 默认
信号处理函数指针 - 捕获
- 返回值:成功返回原信号处理方式,如果之前未处理过则返回NULL,失败返回SIG_ERR。
- 案例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 信号处理函数
void sigfun(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
}
int main(){
// 对2号信号进行忽略处理
if(signal(SIGINT,SIG_IGN)==SIG_ERR){
perror("signal");
return -1;
}
// 对20号进行进行捕获处理
if(signal(SIGTSTP,sigfun)==SIG_ERR){
perror("sigfun");
return -1;
}
for(;;){}
return 0;
}
- 主控制流程、信号处理流程和内核处理流程
- 当有信号到来时,内核会保存当前进程的栈帧,然后再执行信号处理函数
- 当信号处理函数结束后,内核会恢复之前保存的进程的栈帧,使之继续执行
3、太平间信号
无论一个进程是正常终止还是异常终止,都会通过系统内核向其父进程发送SIGCHLD(17)信号。父进程完全可以在针对SIGCHLD(17)信号的信号处理函数中,异步地回收子进程的僵尸,简洁而又高效
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
// 信号处理函数
void sigchild(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
pid_t pid = wait(NULL);
if(pid == -1){
perror("wait");
return ;
}else{
printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
}
}
int main(){
// 父进程对17号信号进行捕获处理
if(signal(SIGCHLD,sigchild)==SIG_ERR){
perror("signal");
return -1;
}
// 父进程创建多个字进程
for(int i=0;i<5;i++){
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0){
printf("%d进程:我是子进程\n",getpid());
sleep(i+1);
return 0;
}
}
for(;;){printf("zpyl\n");sleep(1);}
}
但这样处理存在一个潜在的风险,就是在sigchld信号处理函数执行过程中,又有多个子进程终止,由于SIGCHLD(17)信号不可靠,可能会丢失,形成漏网僵尸,因此有必要在一个循环过程中回收尽可能多的僵尸
- 在信号处理函数执行期间,如果有多个相同的信号到来,只保留一个,其余统统丢弃
// 信号处理函数
void sigchild(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
for(;;){
pid_t pid = waitpid(-1,NULL,WNOHANG);
if(pid == -1){
if(errno == ECHILD){
printf("没有子进程了\n");
break;
}else{
perror("waitpid");
return ;
}
}else if(pid == 0){
printf("%d进程在运行\n",getpid());
break;
}else{
printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
}
}
/*for(;;){
// wait是个阻塞的方法,进程在这里等待子进程死亡,如果子进程一直不死,则会一直等待
pid_t pid = wait(NULL);
if(pid == -1){
if(errno == ECHILD){
printf("没有子进程了\n");
break;
}else{
perror("wait");
return ;
}
}else{
printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
}
}*/
}
4、信号的继承与恢复
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 信号处理函数
void sigfun(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
}
int main(){
// 父进程忽略2号信号
if(signal(SIGINT,SIG_IGN) == SIG_ERR){
perror("SIGINT");
return 0;
}
// 父进程捕获3号信号
if(signal(SIGQUIT,sigfun) == SIG_ERR){
perror("SIGQUIT");
return -1;
}
// 父进程创建子进程
pid_t pid = fork();
if(pid==-1){
perror("fork");
return -1;
}
// 子进程代码
if(pid == 0){
printf("%d进程:我是子进程\n",getpid());
for(;;){}
return 0;
}
// 父进程代码
else{
printf("%d进程:父进程要变身为新进程啦\n",getpid());
if(execl("./new","./new",NULL) == -1){
perror("execl");
return -1;
}
return 0;
}
}
// new
#include <stdio.h>
#include <unistd.h>
int main(){
printf("%d进程:我是新进程\n",getpid());
for(;;);
return 0;
}
- fork函数创建的子进程会继承父进程的信号处理方式。
- 父进程中对某个信号进行捕获,则子进程中对该信号依然捕获
- 父进程中对某个信号进行忽略,则子进程中对该信号依然忽略
- excc家族函数创建的新进程对信号的处理方式和原进程稍有不同
- 原进程中被忽略的信号,在新进程时依然被忽略
- 原进程中被捕获的信号,在新进程中被默认处理
5、发送信号
·用专门的系统命令发送信号
kill [-信号] PID
若不指明具体信号,缺省发送SIGTERM(15)信号
若要指明具体信号,可以使用信号编号,也可以使用信号名称,而且信号名称中的“SIG”前缀可以省略不写。
kill -9 1234
kill -SIGKILL 1234 5678
kill -KILL -1
超级用户可以发给任何进程,而普通用户只能发给自己的进程
相关函数
kill
// 头文件 signal.h
int kill(pid_t pid,int signum);
- 功能:向指定的进程发送信号
- 参数:
- pid可以如下取值
-1 - 向系统中的所有进程发信号
\>0 - 向特定进程(由pid标识)发送信号
- signum:信号编号,取0可用于检查pid进程是否存在,如不存在kill函数会返回-1,且errno为ESRCH
- 返回值:成功(至少发出去一个信号)返回0,失败返回-1
案例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 信号处理函数
void sigfun(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
}
int main(){
// 父进程创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
// 子进程代码,对2号信号进行捕获
if(pid == 0){
if(signal(SIGINT,sigfun) == SIG_ERR){
perror("signal");
return -1;
}
for(;;);
return 0;
}
// 父进程代码,给子进程发送2号信号
getchar();// 等待
if(kill(pid,2)==-1){
perror("kill");
return -1;
}
return 0;
}
判断进程存不存在是不以进程是否死亡为判断标准,当一个子进程死亡时,但是父进程没有进行收尸,则该子进程还是存在的
raise
// 头文件 signal.h
int raise (int signum);
- 功能:向调用进程自己发送信号
- 参数:signum信号编号
- 返回值:成功返回0,失败返回非0
/*kill(getpid(),signum)等价于该函数*/
6、暂停、睡眠与闹钟
6.1 暂停
pause
// 头文件 unistd.h
int pause(void);
- 功能:无限睡眠
- 返回值:成功阻塞,失败返回-1
- 该函数使调用进(线)程进入无时限的睡眠状态,直到有信号终止了该进程或被其捕获。如果有信号被调用进程捕获,在信号处理函数返回以后,pause函数才会返回,其返回值-1,同时置errno为EINTR,表示阻塞的系统调用被信号打断。pause函数要么不返回,要么返回-1,永远不会返回0。
案例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 信号处理函数
void sigfun(int signum){
printf("%d进程:%d号信号开始处理\n",getpid(),signum);
sleep(3);
printf("%d进程:%d号信号处理结束\n",getpid(),signum);
}
int main(){
// 对2号信号进行捕获
if(signal(SIGINT,sigfun)==SIG_ERR){
perror("signal");
return -1;
}
printf("%d进程:一睡不醒\n",getpid());
int res = pause();// 直到有信号了,才会被唤醒执行下面的语句
printf("%d进程:psuse函数返回%d\n",getpid(),res);
return 0;
}
6.2 睡眠
sleep
// 头文件 unistd.h
unsigned int sleep(unsigned int seconds);
- 功能:有限睡眠
- 参数:seconds 以秒为单位的睡眠时限
- 返回值:返回0或剩余秒数。
/* 该函数使调用进程睡眠seconds秒,除非有信号终止了调用进程或被其捕获,如果有信号被调用进程捕获,在信号处理函数返回以后,sleep函数才会返回,且返回值为剩余的秒数,否则该函数将返回0,表示睡眠充足*/
案例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 信号处理函数
void sigfun(int signum){
printf("%d进程:%d号信号开始处理\n",getpid(),signum);
sleep(3);
printf("%d进程:%d号信号处理结束\n",getpid(),signum);
}
int main(){
// 对2号信号进行捕获
if(signal(SIGINT,sigfun)==SIG_ERR){
perror("signal");
return -1;
}
printf("%d进程:一睡不醒\n",getpid());
int res = sleep(10);
printf("%d进程:sleep函数返回%d\n",getpid(),res);
return 0;
}
usleep
// 头文件 unistd.h
int usleep (useconds_t usec);
- 功能:更精确的有限睡眠
- 参数:usec以微秒为单位的睡眠时限
- 返回值:成功返回0,失败返回-1
- 如果有信号被调用进程捕获,在信号处理函数返回以后,usleep函数才会返回,且返回值为-1,同时置errno为EINTR,表示阻塞的系统调用被信号中断
6.3 闹钟
alarm
// 头文件 unistd.h
unsigned int alarm(unsigned int seconds);
- 功能:设置闹钟
- 参数:seconds以秒为单位的闹钟时间。
- 返回值:返回0或先前所设闹钟的剩余秒数。
- alarm函数使系统内核在该函数被调用以后seconds秒的时候,向调用进程发送SIGALRM(14)信号
- 若在调用该函数前已设过闹钟且尚未到期,则该函数会重设闹钟,并返回先前所设闹钟的剩余秒数,否则返回0
- 若seconds!取0,则表示取消先前设过且尚未到期的闹钟
案例
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <signal.h>
// 信号捕获函数
void sigfun(int signum){
time_t now = time(NULL);
struct tm *t;
t = localtime(&now);
printf("\r%4d-%02d-%02d %02d:%02d:%02d",t->tm_year+1900,t->tm_mon+1,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
alarm(1);
}
int main(){
setbuf(stdout,NULL);// 关闭输出缓冲区
if(signal(SIGALRM,sigfun)==SIG_ERR){
perror("signal");
return -1;
}
sigfun(1);
for(;;);
return 0;
}
7、信号集
- 多个信号组成的信号集合谓之信号集
- 系统内核用sigset_t类型表示信号集
- 在<signal.h>中又被定义为typedef __sigset_t sigset_t;
- 在<sigset.h>中有如下类型定义
#define _SIGSET_NWORDS (1024/(8*sizeof(unsigned long int)))
typedef struct{
unsigned long int __val[_SIGSET_NWORDS];
}__sigset_t;
- sigset_t类型是一个结构体,但该结构体中只有一个成员,是一个包含32个元素的整数数组(针对32位系统而言)
·可以把sigset_t类型看成一个由1024个二进制位组成的大整数,其中的每一位对应一个信号,其实目前远没有那么多信号,某位为1就表示信号集中有此信号,反之为0就是无此信号,当需要同时操作多个信号时,常以sigset_t作为函数的参数或返回值的类型
相关函数
sigfillset
// 头文件 signal.h
int sigfillset (sigset_t* sigset);
- 功能:填满信号集,即将信号集的全部信号位置1
- 参数:sigset 信号集
- 返回值:成功返回0,失败返回-1
sigemptyset
// 头文件 signal.h
int sigemptyset(sigset_t* sigset);
- 功能:清空信号集,即将信号集的全部信号位清0
- 参数:sigset信号集
- 返回值:成功返回0,失败返回-1
sigaddset
// 头文件 signal.h
int sigaddset (sigset_t* sigset,int signum);
- 功能:加入信号,即将信号集中与指定信号编号对应的信号位置1
- 参数:
- sigset信号集
- signum:信号编号
- 返回值:成功返回0,失败返回-1
sigdelset
// 头文件 signal.h
int sigdelset (sigset_t* sigset,int signum);
- 功能:删除信号,即将信号集中与指定信号编号对应的信号位清0
- 参数:
- sigset 信号集
- signum 信号编号
- 返回值:成功返回0,失败返回-1
sigismember
// 头文件 signal.h
int sigismember (const sigset_t* sigset,int signum);
- 功能:判断信号集中是否有某信号,即检查信号集中与指定信号编号对应的信号位是否为1
- 参数:
- sigset 信号集
- signum 信号编号
- 返回值:有则返回1,没有返回0,矢败返回-1
案例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 打印一个字节的8位内容
void printb(char byte){
for(int i=0;i<8;i++){
printf("%d",(byte>>(7-i))&0x01);
}
printf(" ");
}
// 打印一块存储区所有字节的比特位
void printm(void* buf,size_t size){
for(int i=0;i<size;i++){
printb(((char*)buf)[size-1-i]);
if((i+1)%8==0){
printf("\n");
}
}
}
int main(){
// 信号集
sigset_t set;
printf("填满信号集\n");
sigfillset(&set);
printm(&set,sizeof(set));
printf("清空信号集\n");
sigemptyset(&set);
printm(&set,sizeof(set));
printf("设置2号信号\n");
sigaddset(&set,SIGINT);
printm(&set,sizeof(set));
printf("删除2号信号\n");
sigdelset(&set,SIGINT);
printm(&set,sizeof(set));
printf("判断2号信号是否存在\n");
printf("%s存在\n",sigismember(&set,SIGINT)==1?" ":"不");
return 0;
}
8、信号屏蔽
- 当信号产生时,系统内核会在其所维护的进程表中,为特定的进程设置一个与该信号相对应的标志位,这个过程就叫做递送(delivery)
- 信号从产生到完成递送之间存在一定的时间间隔,处于这段时间间隔中的信号状态称为未决(pending)
- 每个进程都有一个信号掩码(signal mask),它实际上是一个信号集,位于该信号集中的信号一旦产生,并不会被递送给相应的进程,而是会被阻塞(block)在未决状态
- 在信号处理函数执行期间,这个正在被处理的信号总是处于信号掩码中,如果又有该信号产生,则会被阻塞,直到上一个针对该信号的处理过程结束以后才会被递送
- 当进程正在执行类似更新数据库这样敏感任务时,可能不希望被某些信号中断。这时可以通过信号掩码暂时屏蔽而非忽略掉这些信号,使其一旦产生即被阻塞于未决状态,待特定任务完成后,再回过头来处理这些信号
设置掩码
sigprocmask
// 头文件 signal.h
int sigprocmask (int how,const sigset_t* sigset,sigset_t* oldset);
- 功能:设置调用进程的信号掩码
- 参数:
- how:修改信号掩码的方式,可取以下值
SIG_BLOCK - 将sigset中的信号加入当前信号掩码
SIG_UNBLOCK - 从当前信号掩码中删除sigset中的信号
SIG_SETMASK - 把sigset设置成当前信号掩码
- sigset:信号集,取NULL则忽略此参数
- oldset:输出原信号掩码,取NULL则忽略此参数
- 返回值:成功返回0,失败返回-1
- 案例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
void sigfun(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
}
int main(){
int signum = /*SIGINT*/ 50;
// 对2号信号进行捕获处理
if(signal(signum,sigfun)==SIG_ERR){
perror("signal");
}
// 对2号信号进行屏蔽
printf("父进程增加对信号的屏蔽\n");
sigset_t set;
sigemptyset(&set);// 清空
sigaddset(&set,signum);// 设置信号
sigset_t oldset;// 用来保存设置前的信号集
if(sigprocmask(SIG_BLOCK,&set,&oldset)==-1){
perror("sigprocmake");
return -1;
}
// 创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
// 子进程向父进程发送2号信号
if(pid == 0){
printf("子进程发送信号\n");
for(int i=0;i<5;i++){
kill(getppid(),signum);
printf("发送%d此信号\n",(i+1));
}
return 0;
}
// 父进程更新数据库
for(int i=0;i<5;i++){
printf("父进程保存%d条数据\n",(i+1));
sleep(1);
}
// 父进程接触对2号信号的屏蔽
printf("父进程接触对信号的屏蔽\n");
if(sigprocmask(SIG_SETMASK,&oldset,NULL)==-1){
perror("sigprocmake");
return -1;
}
// 父进程收尸
if(wait(NULL)==-1){
perror("wait");
return -1;
}
return 0;
}
- 对于可靠信号,通过sigprocmask函数设置信号掩码以后,每种被屏蔽信号中的每个信号都会被阻塞,并按先后顺序排队,一旦解除屏蔽,这些信号会被依次递送
- 对于不可靠信号,通过sigprocmask函数设置信号掩码以后,每种被屏蔽信号中只有第一个会被阻塞,并在解除屏蔽后被递送,其余的则全部丢失