作者介绍
张伟伟,男,西安工程大学电子信息学院,2019级硕士研究生,张宏伟人工智能课题组。
微信公众号:可随时查阅,搜索—张二牛的笔记,内容会分类上传。
研究方向:机器视觉与人工智能。
电子邮件:[email protected]
电子邮件:[email protected]
- 课题组CSDN官方账号,欢迎一键三连: https://blog.csdn.net/m0_37758063/article/details/113527955?spm=1001.2014.3001.5501.
学习目标
了解信号的相关概念
熟练使用信号相关函数
参考文档使用信号集操作相关函数
熟练使用信号捕捉函数signal
熟练使用信号捕捉函数sigaction
熟练掌握使用信号完成子进程回收(SIGTIME)
##查看信号
kill -l
1.信号的介绍
1.信号的概念
信号是信息的载体,Linux/UNIX环境下,古老、经典的通信方式。(事先预定好的)
1.2 信号的特点
(1) 简单
(2) 不能携带大量信息
(3) 满足某个特点条件才会产生
1.3 信号的机制
与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。
每个进程收到的所有信号,都是由内核负责发送的。
1.4 信号的状态
信号的三种状态:产生、未决、递达2
信号的处理方式:
—执行默认动作
—忽略信号(丢弃不处理)SIGKILL SIGSTOP(这两个信号不能被阻塞)
—捕捉信号(调用用户的自定义的处理函数)
1.5 信号的产生
按键产生,如:Ctrl+c、Ctrl+z、Ctrl+
系统调用产生,如:kill、raise、abort
软件条件产生,如:定时器alarm
硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
命令产生,如:kill命令
1.6 信号的四要素
- 信号名(singnal)
- 信号编号(value)
- 信号的默认处理动作()
- 信号的描述内容
信号的默认处理动作
Term:终止进程
Ign:忽略信号 (默认即时对该种信号忽略操作)
Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)
Stop:停止(暂停)进程
Cont:继续运行进程
常用的一些信号:kill -l 查看有哪些信号
几个常用到的信号
SIGINT、SIGQUIT、SIGKILL、SIGSEGV、SIGUSR1、SIGUSR2(用户使用)、
SIGPIPE(给没有读端的管道写数据,会产生此信号)、
SIGALRM、SIGTERM、SIGCHLD、SIGSTOP、SIGCONT
1.7 信号的特质
信号的实现手段具有很强的延时性,对用户来说,时间非常短。
3. 信号相关函数
3.1 signal函数
函数作用:注册信号捕捉函数
函数原型
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数参数
signum:信号编号
handler:信号处理函数
singal实例(回调函数使用:tyepdef 函数名指针,类型,再定义一个函数传入)
//signal函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sighandler(int signo)
{
printf("signo==[%d]\n", signo);
}
int main()
{
//注册信号处理函数
signal(SIGINT, sighandler);
//while(1)
{
sleep(10);
}
return 0;
}
3.2 kill函数/命令
描述:给指定进程发送指定信号
kill命令:kill -SIGKILL 进程PID
kill函数原型:int kill(pid_t pid, int sig);
函数返回值:
成功:0;
失败:-1,设置errno
函数参数:
**sig信号参数**:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。 如**SIGKILL**
pid参数:
pid > 0: 发送信号给指定的进程。
pid = 0: 发送信号给与调用kill函数进程属于同一进程组的所有进程。
pid < -1: 取|pid|发给对应进程组。
pid = -1:发送给进程有权限发送的系统中所有进程。
进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。
3.3 abort函数raise函数
raise函数
man 3 abort
raise函数
函说描述:给当前进程发送指定信号(自己给自己发)
函数原型:int raise(int sig);
函数返回值:成功:0,失败非0值
函数拓展:raise(signo) == kill(getpid(), signo);
abort函数
abort函数
函数描述:给自己发送异常终止信号 6) SIGABRT,并产生core文件
函数原型:void abort(void);
函数拓展:abort() == kill(getpid(), SIGABRT);
ls -ltr
ulimit -a
ulimit -c ulimited
gdb -g core
run
exit
3.4 alarm函数(阻塞函数遇到信号都会被中断)
被信号终端都会出现一个Inrterrupted function calll的错误
man 2 alarm
函数原型:unsigned int alarm(unsigned int seconds);
函数描述:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
**每个进程都有且只有唯一的一个定时器。**
函数返回值:返回0(第一次调用)或剩余的秒数(第2次调用),无失败。
常用操作:取消定时器alarm(0),返回旧闹钟余下秒数。
alarm使用的是自然定时法,与进程状态无关,就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。
3.5 alarm函数实例;捕捉信号,隔五秒运行回调函数
//signal函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sighandler(int signo)
{
printf("signo==[%d]\n", signo);
}
int main()
{
//注册信号处理函数
signal(SIGINT, sighandler);
signal(SIGALRM, sighandler);
int n = alarm(5);
printf("first: n==[%d]\n", n);
sleep(2);
n = alarm(5);
//n = alarm(0); //取消时钟
printf("second: n==[%d]\n", n);
while(1)
{
sleep(10);
}
return 0;
}
3.6 settimer函数
函数原型
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
函数描述
设置定时器(闹钟),可代替alarm函数,精度微秒us,可以实现周期定时。
函数返回值
成功:0;
失败:-1,设置errno值
函数参数:
which:指定定时方式
自然定时:ITIMER_REAL → 14)SIGALRM计算自然时间 常用
虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用cpu的时间
运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间
new_value:struct itimerval, 负责设定timeout时间。
itimerval.it_value: 设定第一次执行function所延迟的秒数 itimerval.it_interval: 设定以后每几秒执行function
struct itimerval {
struct timerval it_interval; // 闹钟触发周期
struct timerval it_value; // 闹钟触发时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
}
old_value: 存放旧的timeout值,一般指定为NULL
练习: 使用setitimer实现每隔一秒打印一次hello, world。
#include <sys/time.h>
#include <stdio.h>
#include<string.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
void sighandler(int sig)
{
printf("signum:[%d] hello world \n",sig);
}
int main()
{
signal(SIGALRM, sighandler);
//int setitimer(int which, const struct itimerval *new_value,
// struct itimerval *old_value);
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;
//注册一个时钟
int retSetitimet = setitimer(ITIMER_REAL, &tm , NULL);
while(1)
{
sleep(1);
}
return 0;
}
4. 信号集相关
信号的三种状态:产生 未决 抵达
- 未决信号集中保存的是未被处理的信号
- 阻塞信号集是当前进程要阻塞的信号的集合
- 这两个集合存储在内核的PCB中
4.1 信号未决信号集和阻塞信号集的关系
当进程收到一个SIGINT信号(信号编号为2),首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1:
如果为1,表示SIGNIT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集上该位置上的值保持为1,表示该信号处于未决状态;
如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上将1变为0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。
当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理
4.2 信号集相关函数总结
sigset_t 类型
上述变量类型的定义的查找有个小窍门: 可以执行gcc的预处理命令:
gcc -E test.c -o test.i 这样头文件就会展开,可以直接到test.i文件中看到相关变量类型的定义。
信号集相关函数
int sigemptyset(sigset_t *set);
函数说明:将某个信号集清0
函数返回值:成功:0;失败:-1,设置errno
int sigfillset(sigset_t *set);
函数说明:将某个信号集置1
函数返回值:成功:0;失败:-1,设置errno
int sigaddset(sigset_t *set, int signum);
函数说明:将某个信号加入信号集合中
函数返回值:成功:0;失败:-1,设置errno
int sigdelset(sigset_t *set, int signum);
函数说明:将某信号从信号清出信号集
函数返回值:成功:0;失败:-1,设置errno
int sigismember(const sigset_t *set, int signum);
函数说明:判断某个信号是否在信号集中
函数返回值:在:1;不在:0;出错:-1,设置errno
sigprocmask函数****(非常重要 )****
函数说明:用来屏蔽信号、解除屏蔽也使用该函数。其本质,读
取或修改进程控制块中的信号屏蔽字(阻塞信号集)。
特别注意,屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。
函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数返回值:成功:0;失败:-1,设置errno
函数参数:
how参数取值:假设当前的信号屏蔽字为mask
SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
set:传入参数,是一个自定义信号集合。由参数how来指示如何修改当前信号屏蔽字。
oldset:传出参数,保存旧的信号屏蔽字。
sigpending函数
int sigpending(sigset_t *set);
函数说明:读取当前进程的未决信号集
函数参数:set传出参数
函数返回值:成功:0;失败:-1,设置errno
阻塞或者不阻塞是用户进程说了算
4.3 实例:编写程序设置阻塞信号集并把常规信号的未决状态打印到屏幕,每隔十次设置为非阻塞,然后调用信号回调函数
大致过程:2 3 阻塞信号集 在循环里循环,按键盘产生信号,未决信号集中
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
void sighandler(int sig)
{
printf("2号 3号!");
}
int main()
{
//注册信号处理函数
signal(SIGINT, sighandler);
signal(SIGQUIT, sighandler);
//定义未决信号集
sigset_t set;
//初始化信号集
sigemptyset(&set);
//将SIGINT SIGQUIT加入到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
//加入阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
sigset_t pend;
int i = 0;
int j = 1;
while(1)
{
//获取未决信号集
sigemptyset(&pend);
sigpending(&pend);
for(i=1; i < 32 ; i++)
{
if(sigismember(&pend,i) == 1)
{
printf("1");
}
else
{
printf("0");
}
}
if(j++%10 == 0)
{
sigprocmask(SIG_UNBLOCK, &set, NULL );
}
else
{
sigprocmask(SIG_BLOCK, &set, NULL);
}
sleep(1);
printf("\n");
}
return 0;
}
4.4 信号捕捉函数
signal函数
函数说明:注册一个信号处理函数
sigaction函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
函数参数:
signum:捕捉的信号
act: 传入参数,新的处理方式。
oldact: 传出参数,旧的处理方式
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理函数
sigset_t sa_mask; //信号处理函数执行期间需要阻塞的信号
int sa_flags; //通常为0,表示使用默认标识
void (*sa_restorer)(void);
};
总结:
sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作
sa_mask: 用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
sa_flags:通常设置为0,使用默认属性。
sa_restorer:已不再使用
- 示例
//sigaction函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sighandler(int signo)
{
printf("signo==[%d]\n", signo);
sleep(4);
}
int main()
{
//注册信号处理函数
struct sigaction act;
act.sa_handler = sighandler;
sigemptyset(&act.sa_mask); //在信号处理函数执行期间, 不阻塞任何信号
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
signal(SIGQUIT, sighandler);
while(1)
{
sleep(10);
}
return 0;
}
4.5 SIGCHLD信号(父进程利用此信号对子进程进行回收)
4.5.1 产生SIGCHLD信号
- 子进程结束的时候
- 子进程收到SIGSTOP信号 kill -CONT
- 当子进程停止时,收到SIGCONT kill -9
4.5.2 SIGCHLD信号信号的作用
子进程退出后,内核会给它的父进程发送SIGCHLD信号,父进程收到这个信号后可以对子进程进行回收。
使用SIGCHLD信号完成对子进程的回收可以避免父进程阻塞等待而不能执行其他操作,只有当父进程收到SIGCHLD信号之后才去调用信号捕捉函数完成对子进程的回收,未收到SIGCHLD信号之前可以处理其他操作。
4.5.3 实例:重要,父进程使用信号回收子进程
信号不排队,且阻塞。
先阻塞,然后信号注册后再解除再解除阻塞。
细节:需要注意的问题.
有可能还未完成信号处理函数的注册三个子进程都退出了。
解决办法:可以在fork之前先将SIGCHLD信号阻塞,当完成信号处理函数的注册后在解除阻塞。
当SIGCHLD信号函数处理期间, SIGCHLD信号若再次产生是被阻塞的,而且若产生了多次, 则该信号只会被处理一次, 这样可能会产生僵尸进程。
解决办法: 可以在信号处理函数里面使用while(1)循环回收, 这样就有可能出现捕获一次SIGCHLD信号但是回收了多个子进程的情况,从而可以避免产生僵尸进程
//父进程使用SICCHLD信号完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void waitchild(int signo)
{
pid_t wpid;
//回收子进程
while(1)
{
wpid = waitpid(-1, NULL, WNOHANG);
if(wpid>0)
{
printf("child is quit, wpid==[%d]\n", wpid);
}
else if(wpid==0)
{
printf("child is living, wpid==[%d]\n", wpid);
break;
}
else if(wpid==-1)
{
printf("no child is living, wpid==[%d]\n", wpid);
break;
}
}
}
int main()
{
int i = 0;
int n = 3;
//将SIGCHLD信号阻塞,防止信号未注册,进程已结束,放在进程前面
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);
for(i=0; i<n; i++)
{
//fork子进程
pid_t pid = fork();
if(pid<0) //fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid>0) //父进程
{
printf("father: fpid==[%d], cpid==[%d]\n", getpid(), pid);
sleep(1);
}
else if(pid==0) //子进程
{
printf("child: fpid==[%d], cpid==[%d]\n", getppid(), getpid());
break;
}
}
//父进程
if(i==3)
{
printf("[%d]:father: fpid==[%d]\n", i, getpid());
//signal(SIGCHLD, waitchild);
//注册信号处理函数
struct sigaction act;
act.sa_handler = waitchild;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sleep(5);
sigaction(SIGCHLD, &act, NULL);
//完成注册后,再解除对SIGCHLD信号的阻塞
sigprocmask(SIG_UNBLOCK, &mask, NULL);
while(1)
{
sleep(1);
}
}
//第1个子进程
if(i==0)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
//sleep(1);
}
//第2个子进程
if(i==1)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
sleep(1);
}
//第3个子进程
if(i==2)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
sleep(1);
}
return 0;
}
4.6 内核捕捉信号的流程
有一定的延时性
2中会有信号的未决集
4.7. 实例:使用信号两个进程进行交替数数
看明白了
子进程父进程来回发,通过flag交替
//使用SIGUSR1和SIGUSR2在父子进程间交替数数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
int num = 0;
int flag;
void func1(int signo)
{
printf("F:[%d]\n", num);
num += 2;
flag = 0;
sleep(1);
}
void func2(int signo)
{
printf("C:[%d]\n", num);
num += 2;
flag = 0;
sleep(1);
}
int main(int argc, char *argv[])
{
int ret;
pid_t pid;
pid = fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid>0)
{
num=0;
flag = 1;
signal(SIGUSR1, func1);
while(1)
{
if(flag==0)
{
kill(pid, SIGUSR2);
flag = 1;
}
}
}
else if(pid==0)
{
num=1;
flag = 0;
signal(SIGUSR2, func2);
while(1)
{
if(flag==0)
{
kill(getppid(), SIGUSR1);
flag = 1;
}
}
}
}