信号
信号的机制
进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕后在继续运行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断。
man 7 signal
信号的产生
- 按键产生:
ctrl+c
、ctrl+z
、ctrl+\
- 系统调用:kill、raise、abort
- 软件条件产生:定时器alarm
- 硬件异常产生:非法访问内存(段错误)、除0、内存对齐出错(总线错误)
- 命令产生:kill
信号的处理动作
- 执行默认动作
- 忽略信号(丢弃不处理)
- 捕捉信号(调用用户的自定义处理函数)
注意:
1、SIGKILL和SIGSTOP不能被捕获、阻塞及忽略;
2、信号有很强的延时性(因为需要内核发出),但对于用户来说感觉不到;
3、A用户的进程不能给B用户的进程发送信号,但A用户下的进程可以相互发送信号;
信号相关函数
- signal——注册信号捕捉函数
给内核注册,是信号产生后内核去调用的函数,捕获后就不在执行信号原有的默认动作。
typedef void (*sighandle_t)(int);
sighandle_t signal(int signum, sighandle_t handle);
param:
signum: 信号
handle: 信号处理函数
也可以是SIG_DFL表示执行默认动作;或者SIG_IGN表示忽略信号
- kill——给指定进程发送信号
命令:
kill -signum PID
函数:
int kill(pid_t pid, int sig);
param:
pid>0: 发送信号给指定的进程
pid=0: 发送信号给与调用kill进程属于同组的所有进程(兄弟进程)
return:
成功:0
失败:-1
- raise——给当前进程发送指定信号(自己给自己发)
int raise(int sig);
- abort——给自己发送异常终止信号SIGABRT(6),并产生core文件
void abort();
如果没有产生core文件,先查看core文件是不是设置为0了
ulimit -a
如果是0,则需要更改大小,一般改为unlimited
ulimit -c unlimited
- alarm——设置定时器,在指定seconds后,内核会给当前进程发送SIGALRM信号
alarm使用的是自然定时法,与进程状态无关,就绪、运行、挂起、终止、僵尸……,无论进程处于何种状态,alarm都计时
进程收到该信号,默认动作终止,每个进程都有且只有唯一的一个定时器
unsigned int alarm(unsigned int seconds);
return
返回0或者剩余的秒数
alarm(0): 取消定时器,返回旧闹钟剩余的秒数
alarm(5) ---> return 0
sleep(2)
alarm(5) ---> return 5-2
例如:1S内可以打印出多少个字符
int main()
{
int i=0;
alarm(1);
while(1)
{
printf("[%d]\n", i++);
}
return 0;
}
time ./a.out
real 0m1.001s ---> 实际执行时间
user 0m0.028s ---> 用户时间
sys 0m0.268s ---> 系统时间
time ./a.out
real 0m1.001s
user 0m0.028s
sys 0m0.268s
损耗时间:1.001-0.028-0.268=0.705
//利用输出重定向到文件看能打印多少个字符
time ./a.out > test.log
real 0m1.259s
user 0m0.089s
sys 0m0.883s
损耗时间:1.259-0.089-0.883=0.287
出现这种情况的原因是:调用printf函数遇到\n就会刷新缓冲区,然后打印,会从用户区切换到内核区,切换次数越多消耗时间越长;而使用文件重定向,由于文件重定向是带有缓冲的,写满缓冲之后才会写回文件中,切换的次数也减少,从而使损耗降低。
- setitimer——设置定时器(同alarm),精度us,可以实现周期定时触发SIGALRM信号
int setitimer(int which, const struct itimerval* newValue, struct itimerval* oldValue);
return:
成功:0
失败:-1
param:
which: 指定定时方式
自然定时法——ITIMER_REAL,计算自然时间,同alarm
虚拟空间计时(用户)——ITIMER_VIRTUAL,只计算进程占用CPU的时间
运行时间计时(用户+内核)——ITIMER_PROF,计算占用CPU及执行系统调用的时间
newValue: 负责设定的时间
oldValue: 存放旧的timeout,一般传入NULL
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/time.h>
void handle(int signum)
{
printf("signum=[%d]\n", signum);
}
int main()
{
signal(SIGALRM, handle);
struct itimerval tm;
//周期性时间赋值
tm.it_interval.tv_sec=1;
tm.it_interval.tv_usec=0;
//第一次触发时间赋值
tm.it_value.tv_sec=3;
tm.it_value.tv_usec=0;
setitimer(ITIMER_REAL, &tm , NULL);
while(1)
{
sleep(1);
}
return 0;
}
//程序在3S后,每隔一秒触发一次信号处理函数
信号集
信号集:未决信号集合和阻塞(屏蔽)信号集(系统能处理的信号集合,每个bit对应一个信号标识位)
未决信号:产生和递达之间的状态,主要由于阻塞(屏蔽)导致该状态
当进程收到信号时,首先被保留在未决信号集中,此时未决信号集中对应的标识位为1,当这个信号被处理之前,先检查阻塞信号集中对应编号位上的标识位是否为1:为1表示该信号被当前进程阻塞,此时该信号暂时不被处理,对应的未决信号集仍然为1;为0表示该信号未被当前进程阻塞,则未决信号集中的信号需要被处理(忽略、执行默认动作、捕获),当信号被处理完成后,未决信号集中的标识位从1变为0,表示该信号已经递达了(已经被处理了)。如果该信号被阻塞期间又收到多个同样的信号,最终只会保留一个(因为未决信号集中只能标识0和1)
屏蔽信号只是将信号处理延后执行(延至解除屏蔽),而忽略表示将信号丢弃处理
信号集相关函数
清空信号集,将某个信号集清0
int sigemptyset(sigset_t *set);
将信号集所有位置1
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);
屏蔽、解除屏蔽函数(修改的是阻塞信号集)
只有这个函数才是修改内核变量,其余都是修改本地变量
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
param:
mask表示当前的信号屏蔽字
how: SIG_BLOCK: set需要屏蔽的信号mask=mask|set
SIG_UNBLOCK: set需要解除屏蔽的信号mask=mask&~set
SIG_SETMASK: set为替换原始屏蔽集的新屏蔽集
set: 自定义的信号集
oldset: 传出参数,保存旧的信号集
读取当前进程的未决信号集(将内核中的数据拷贝到本地)
int sigpending(sigset_t *set);
例:用键盘产生SIGINT、SIGQUIT信号后,将它们阻塞,每10次循环解除阻塞一次,并且执行信号处理函数。多次产生信号,在解除阻塞后只会处理一次
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
void handle(int signum)
{
printf("signum=[%d]\n", signum);
}
int main()
{
signal(SIGINT, handle);
signal(SIGQUIT, handle);
//定义信号集
sigset_t set;
//初始化信号集
sigemptyset(&set);
//将SIGINT SIGKILL加入信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
//将set集中的信号加入到阻塞信号集中
sigprocmask(SIG_BLOCK, &set, NULL);
sigset_t pend;
int i=1;
int j=0;
while(1)
{
//获取未决信号集
sigemptyset(&pend);
sigpending(&pend);
for(i=1;i<32;i++)
{
if(sigismember(&pend, i)==1)
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
if(++j%10==0)
{
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
else
{
sigprocmask(SIG_BLOCK, &set, NULL);
}
sleep(1);
}
return 0;
}
//键盘输入ctrl+c和ctrl+\会将这两个信号加入阻塞集中,从而不会执行信号处理函数
//每10次循环会解除一次阻塞,然后执行相应的信号处理函数
//相同信号只算一次,不会排队
sigaction
注册一个信号处理函数,同signal函数
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
param:
act: 新的处理方式
oldact: 旧的处理方式
struct sigaction
{
void (*sa_handler)(int);//信号处理函数
sigset_t sa_mask;//用来指定在信号处理函数执行期间需要被屏蔽的信号,当某个信号被处理时,这个信号不会再度发生(仅在处理函数调用期间生效)
int sa_flags;//默认为0
}
在XXX信号处理函数执行期间,若XXX信号再次产生多次,那么信号处理函数也不会被打断,当信号处理函数执行完后,后来产生的信号只会被处理一次(信号不支持排队)
例:在按Ctrl+C后,会进入信号处理函数,执行sleep(3)期间,多次执行ctrl+c也没有任何反应,等sleep结束后,会再次调用信号处理函数
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
void handle(int signum)
{
printf("signum=[%d]\n", signum);
sleep(3);
}
int main()
{
struct sigaction act;
act.sa_handler = handle;//信号处理函数
sigemptyset(&act.sa_mask);//阻塞的信号
act.sa_flags=0;
sigaction(SIGINT, &act, NULL);
while(1)
{
sleep(1);
}
return 0;
}
在XXX信号处理函数执行期间(前提是阻塞了YYY信号),若收到YYY信号,则YYY信号会被阻塞,当XXX信号处理函数执行完毕后,YYY信号只会被处理一次。
例:在按Ctrl+C后,会进入信号处理函数,执行sleep(3)期间,多次执行ctrl+\也没有任何反应,等sleep结束后,才会调用信号处理函数处理ctrl+\信号
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
void handle(int signum)
{
printf("signum=[%d]\n", signum);
sleep(3);
}
int main()
{
struct sigaction act;
act.sa_handler = handle;//信号处理函数
sigemptyset(&act.sa_mask);//阻塞的信号
sigaddset(&act.sa_mask, SIGQUIT);//在信号处理函数执行期间阻塞SIGQUIT信号
//如果没有将SIGQUIT加入阻塞集中,那么在处理SIGINT期间按下ctrl+\就会退出程序
act.sa_flags=0;
sigaction(SIGINT, &act, NULL);
signal(SIGQUIT, handle);
while(1)
{
sleep(1);
}
return 0;
}
SIGCHLD信号
产生条件
- 子进程结束
- 子进程收到SIGSTOP信号(暂停)
- 当子进程停止时,收到SIGCONT信号
SIGCHLD作用:子进程退出后,内核会给它的父进程发送SIGCHLD信号,父进程收到这个信号可以对子进程进行回收。
使用SIGCHLD信号完成对子进程回收的优点是:可以避免父进程阻塞等待而不能执行其他操作,只有收到SIGCHLD信号后才会对它进行处理,没收到SIGCHLD信号之前可以处理其它操作。
例:父进程产生三个子进程,子进程结束时会产生SIGCHLD信号,父进程收到信号来回收子进程,避免产生僵尸进程
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
#include<unistd.h>
void waitchild(int signum)
{
pid_t wpid=waitpid(-1, NULL, WNOHANG);
//WNOHANG不能换成0,因为SIGSTOP和SIGCONT也会产生SIGCHLD信号,那样会一直阻塞在这里
if(wpid>0)
{
printf("child is quit, wpid==[%d]\n", wpid);
}
else if(wpid==0)
{
printf("child is living\n");
}
else if(wpid==-1)
{
printf("no child is alive\n");
}
}
int main()
{
pid_t pid;
int i=0;
for(i=0;i<3;i++)
{
pid=fork();
if(pid>0)
{
}
else
{
printf("child process [%d]\n", getpid());
break;
}
}
if(i==0)
{
printf("the first child [%d]\n", getpid());
sleep(1);
}
if(i==1)
{
printf("the second child [%d]\n", getpid());
sleep(2);
}
if(i==2)
{
printf("the third child [%d]\n", getpid());
sleep(3);
}
if(i==3)
{
struct sigaction act;
act.sa_handler = waitchild;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGCHLD, &act, NULL);
while(1)
{
sleep(1);
}
}
return 0;
}
child process [4260]
the first child [4260]
child process [4261]
child process [4262]
the third child [4262]
the second child [4261]
child is quit, wpid==[4260]
child is quit, wpid==[4261]
child is quit, wpid==[4262]
^C
例:信号处理函数延时2S,那么在延时期间再收到SIGCHLD信号后再次收到会阻塞,最后只会处理一个。因此只有两个进程回收,有一个进程成为了僵尸进程
void waitchild(int signum)
{
pid_t wpid=waitpid(-1, NULL, WNOHANG);
if(wpid>0)
{
printf("child is quit, wpid==[%d]\n", wpid);
}
else if(wpid==0)
{
printf("child is living\n");
}
else if(wpid==-1)
{
printf("no child is alive\n");
}
sleep(2);
}
int main()
{
pid_t pid;
int i=0;
for(i=0;i<3;i++)
{
pid=fork();
if(pid>0)
{
}
else
{
printf("child process [%d]\n", getpid());
break;
}
}
if(i==0)
{
printf("the first child [%d]\n", getpid());
//sleep(1);
}
if(i==1)
{
printf("the second child [%d]\n", getpid());
//sleep(2);
}
if(i==2)
{
printf("the third child [%d]\n", getpid());
//sleep(3);
}
if(i==3)
{
struct sigaction act;
act.sa_handler = waitchild;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGCHLD, &act, NULL);
while(1)
{
sleep(1);
}
}
return 0;
}
例:解决办法:采用while循环,来将信号处理函数执行期间退出的子进程回收掉
void waitchild(int signum)
{
while(1)//因为信号不能排队,即使收到3个信号也能全回收
{
pid_t wpid=waitpid(-1, NULL, WNOHANG);
if(wpid>0)
{
printf("child is quit, wpid==[%d]\n", wpid);
}
else if(wpid==0)
{
printf("child is living\n");
break;
}
else if(wpid==-1)
{
printf("no child is alive\n");
break;
}
}
}
int main()
{
pid_t pid;
//将SIGCHLD信号阻塞(父进程故意要让子进程先退出,所以要先将信号阻塞起来)
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);
int i=0;
for(i=0;i<3;i++)
{
pid=fork();
if(pid>0)
{
}
else
{
printf("child process [%d]\n", getpid());
break;
}
}
if(i==0)
{
printf("the first child [%d]\n", getpid());
}
if(i==1)
{
printf("the second child [%d]\n", getpid());
}
if(i==2)
{
printf("the third child [%d]\n", getpid());
}
if(i==3)
{
struct sigaction act;
act.sa_handler = waitchild;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sleep(5);//故意先让子进程都退出,所以要在函数开始先让SIGCHLD信号阻塞
sigaction(SIGCHLD, &act, NULL);
//完成SIGCHLD信号注册后,解除对SIGCHLD的阻塞
sigprocmask(SIG_UNBLOCK, &mask, NULL);
while(1)
{
sleep(1);
}
}
return 0;
}
综上:
1、有可能还未完成信号处理函数的注册,子进程就已经退出了;
------可以在fork之前先将SIGCHLD信号阻塞,当完成信号处理函数的注册后,再解除阻塞
2、当SIGCHLD信号处理函数执行期间,SIGCHLD信号若再次产生是被阻塞的,而且即使产生多次,该信号也只会被处理一次,这样可能会产生僵尸进程
------信号处理函数使用while循环回收,循环回收已经退出的子进程,但要注意wpid\ == 0和wpid == -1加break退出循环
利用信号完成父子进程间通信
使用用户自定义的信号SIGUSR1和SIGUSR2来完成通信,父子进程在收到各自的信号后完成计数
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
#include<unistd.h>
int num=0;
int flag;
void func1(int signum)
{
printf("F:[%d]\n", num);
num+=2;
flag=0;
sleep(1);
}
void func2()
{
printf("C:[%d]\n", num);
num+=2;
flag=0;
sleep(1);
}
int main()
{
int ret;
pid_t pid;
pid=fork();
if(pid>0)
{
num=0;
flag=1;
signal(SIGUSR1, func1);
while(1)
{
if(flag==0)
{
kill(pid, SIGUSR2);
flag=1;
}
}
}
else
{
num=1;
flag=0;
signal(SIGUSR2, func2);
while(1)
{
if(flag==0)
{
kill(getppid(), SIGUSR1);
flag=1;
}
}
}
return 0;
}