Linux之系统编程之进程信号

一、信号

1.信号是什么

  • 信号是一个软件中断,通知进程发生了某个事件,中断当前进程正在执行操作,去处理这个事件。
  • 信号就是代表着事件。
  • 信号有多种,各自表示不同的事件。

2.信号的生命周期

  • 信号的产生=》进程中注册=》信号的销毁(在进程中注销)=》信号的处理。
  • 信号的阻塞(暂不处理该信号)

3.信号的种类

  • kill -l查看信号种类
  • 62种信号。
  • 非可靠信号:1~31,有可能会造成事件的丢失。从unix借鉴而来的,每个信号都有具体的对应的系统事件、
  • 可靠信息:34~64,不会造成信号的丢失。后期扩充的,因为没有具体的对应的事件,因此命名比较草率。
  • 在所有的信号中,有两个信号比较特殊,SIGKILL -9 / SIGSTOP -19,这两个信号不可被阻塞,不可被忽略,不可被自定义。

4.信号的产生

  • core dumped:(核心转储功能)当进程非正常终止时,会存储进程的运行数据,目的是为了方便进程的事后调试,默认是关闭的。
  • ulimit -a:查看进程中的一些限制信息。
  • ullimit -c:设置core dumped文件的最大大小。

(1)硬件产生

  • Ctrl + c(中断当前操作)SIGINT(2号)
  • Ctrl + |(退出操作)SIGOUT(3号)
  • Ctrl + z(将当前进程停止运行)SIGSTP(20号)

(2)软件产生

  • kill -signum pid 如 kill -9(强制杀死进程)发送9号信号SIGKILL
  • 为什么停止状态进程 kill pid杀不掉,因为kill pid是向pid进程发送一个终止信号,进程收到这个信号并且处理这个信号才会退出,然而停止状态的进程,当前什么都不干,意味着也不会去处理信号。而kill -9可以杀死他,因为向进程发送9号信号(SIGKILL),强制杀死信号,必须处理的一个信号
  • kill杀死一个进程的原理:向进程发送一个信号,信号有对应的事件,进程放下手头工作去处理这个事件,然而这个事件的处理结果就是让进程退出。
  • 代码中的接口:
函数原型意义
int kill(pid_t pid, int signum)pid是进程ID,signum是信号量,向任意进程发送任意信号。
int raise(int sig) = int kill(getpid(), sig)给进程自身发sing信号。
void abort(void)给进程自身发送一个SIGABRT信号,引发当前进程正常终止(出错的时候才用)。
unsigned int alarm(unsigned int second)定时器,定时second秒后给进程发送SIGALAM信号,让该进程运行second秒后退出。
  • 给自己发送信号 kill(getpid(), SIGHUP),raise(SIGHUP)。
  • kill默认发送15号Terminatred终止信号。
  • fg的功能是将一个后台作业调到前台运行。

5.信号在进程中注册

  • pcb中有一个struct sigpending结构体中的结构体struct sigset_t(这个结构体中只有一个数组成员,这个数组用于实现位图)。----这个位图称之为未决信号集合,收到了但是没有被处理的信号集合。
  • 若给一个进程发送一个信号,就会将这个位图中对应位置置1,表示当前进程收到啦这个信号。
  • 位图只有0/1,也就是只能表示是否收到了这个信号,但无法表示收到了多少个这样的信号,信号的注册不仅会修改位图,还会为信号组织一个sigqueue节点添加到pcb的sigqueue链表中。
  • 操作系统修改进程pcb中的一个信号标志位。(位图中)
  • pending未决信号集合。(未决:从产生到处理之间信号所处的一种状态)
  • 注册:在未决信号集合中修改对应位图,并且向sigqueue链表中添加信号节点。
  • 非可靠信号:若当前未决信号集合中指定信号已注册,则什么都不做。若信号注册的时候位图为0,则会创建一个sigqueue节点并修改位图为1,若位图为1,则什么都不做。
  • 可靠信号:不管当前信号是否已经注册(位图当前是否为0),都会去修改位图,添加一个新的sigqueue节点,并修改位图。
  • 只有相同的信号才会考虑是否注册的问题。

6.信号在进程中注销(修改未决信号集合)

  • 为了保证一个信号只会被处理一次,因此先注销再处理。防止正在处理这个信号的时候,别的进程可能也会处理这个信号。
  • 注销:在pcb中删除当前信号信息。删除当前信号的一个sigqueue节点,并且修改位图。
  • 非可靠信号:因为非可靠信号只有一个节点,因此删除节点后,直接将位图置0。
  • 可靠信号:因为可靠信号有可能注册多次,有多个节点,因此删除节点后,判断是否还有相同的信号节点,若没有才会将位图置0。
  • 信号并不是立即被处理的,如果同时来了多个信号,都去注册,只会注册一次。

7.信号的处理(信号的递达)

  • 信号的处理:信号表示一个事件的到来,处理事件就是完成功能。每个信号都对应有自己的事件处理函数,信号到来,去处理这个事件就是去执行这个处理函数;执行完毕事件就处理完了。
  • 处理方式有三种:
    ① 默认处理方式:操作系统中原定义好的每个信号的处理方式。
    ② 忽略处理方式:处理方式就是忽略,什么都不做。
    ③ 自定义处理方式:自己定义一个事件函数,使用这个函数替换内核中默认的处理函数。信号到来就会调用我们定义的函数了。
  • 修改信号处理方式:
    sighandler_t signal(int signum, sighandler_t handler);
    int sigaction(int signum, const struct sigaction* act, struct sigaction* oldaction);
    在这里插入图片描述

8.自定义处理方式的信号捕捉流程

  • 从用户态切换到内核态的方式:当前主控流程要是出现了中断,异常或系统调用。
  • 内核态完成功能之后,从内核态返回用户态之前,要用接口do_signal()检查是否有信号待处理,若是忽略的或者默认处理方式,则在内核中就可以完成。但若是自定义的处理方式,则在do_signal()接口调用完之后返回用户态中的sigcb()回调函数运行,运行完毕之后,调用sys_sigreturn()函数返回内核态,之后再次调用do_signal()接口检查,如果没有信号待处理,则返回用户态的主控流程继续运行代码。
    在这里插入图片描述

8.信号阻塞(生命周期之外的动作)

  • 功能:阻止一个信号被递达(信号依然可以注册,只是暂时不被处理)
  • 阻塞位图block,如果block里该信号位里置1了,就表示该信号已被阻塞,暂时不处理。
  • 阻塞一个信号:只需要在pcb中的block位图中将这个信号标记起来就行。
    在这里插入图片描述
  • 如何阻塞一个接口:int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
  • how:
    ① SIG_BLOCK blocked | set — 将set集合中的信号添加到内核中的block阻塞信号集合中,使用old保存原来的阻塞信息以便还原。阻塞set集合中的和原来block阻塞信号集合中的信号。
    ② SIG_UNBLOCK blocked & (~set) — 将set集合中的信号从内核中的block阻塞信号集合移除,对set集合中的信号解除阻塞。
    ③ SIG_SETMASK blocked = set — 将内核中的block阻塞信号集合内容设置成为set集合中的信息,阻塞set集合中的信号,原来在bloc阻塞集合中的而不在set集合中便不再阻塞。
  • 在所有信号中,有两个信号(9:SIGKILL和19:SIGSTOP)无法被阻塞,无法被定义,无法被忽略。

(1)将一些信号的处理函数自定义:

  • 将所有的信号都给阻塞。
  • 在解除阻塞之前,给进程发送信号。
  • 解除阻塞,查看信号的处理情况。
函数原型含义
int sigemptyset(sigset_t* set);清空set信号集合,使用一个变量的时候的初始化过程。
int sigaddset(sigset_t* set, int signum);向set集合中添加指定的信号
int sigfillset(sigset_t* set);将所有信号添加到set集合中
int sigdelset(sigset_t* set, int signum);从set集合中移除指定的集合
int sigismember(const sigset_t* set, int signum);判断指定信号是否在set集合中
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void sigcb(int signum)
{
 	printf("recv a signal: %d\n", signum);
}
int main()
{
	signal(SIGINT, sigcb);
	signal(SIGRTMIN+4, sigcb);

	sigset_t set;
	sigemptyset(&set);//清空集合,防止未知数据造成影响
	sigfillset(&set);//向集合中添加所有信号

	sigprocmask(SIG_BLOCK, &set, NULL);//阻塞set集合中的所有信号
	printf("press enter continue!\n");
	getchar();//等待一个回车,如果不按回车就一直卡在这里

	sigprocmask(SIG_UNBLOCK, &set, NULL);//解除set集合中的信号阻塞
	while(1)
	{
		sleep(1);
	}
	
	return 0;
}

二、竞态条件

  • 在多个执行流中,对同一段代码进行竞争执行。

1.函数的可重入和不可重入

  • 函数的重入:在多个执行流中,重复进入同一个函数执行代码。

  • 函数的不可重入:如果一个函数重入之后,可能会造成数据二义或程序的逻辑紊乱,这个函数则是不可重入的。

  • 函数的可重入:函数重入之后不会出现任何影响。

  • 可重入与不可重入的判断依据:在这个函数中是否对全局数据进行了非原子安全操作。

  • 当自己设计函数或者使用别人函数的时候,根据使用场景就要考虑函数的重入状况。

  • volatile关键字:用于修饰一个变量,保持变量的内存可见性,防止编译器过度优化。(每次都从内存中重新获取被修饰的变量的数据。)

2.SIGCHLD信号(非可靠信号,位图中只标记了一次)

  • 子进程退出时,操作系统给父进程发送SIGCHLD信号,因为SIGCHLD信号的默认处理方式就是什么都不做,因此相当于没有关注到子进程的退出状态,因此导致了子进程称为了僵尸进程,否则就要进行进程等待(一直阻塞等待)。
  • 僵尸进程:子进程退出后会向父进程发送SIGCHLD信号通知父进程,子进程状态改变,但是因为SIGCHLD信号,默认的处理方式是忽略;因此之前的程序中若不进行进程等待则不知道子进程的退出,如果进行进程等待,而且不想让父进程阻塞,就可以自定义SIGCHLD信号的处理方式;在自定义回调函数中调用waitpid,处理僵尸进程,父进程不用一直等待。但是SIGCHLD信号是一个非可靠信号,如果有多个子进程同时退出,有可能造成信号丢失(如有10个子进程同时退出的时候,只能处理一个 )while(wairpid(-1, NULL, WNOHANG) > 0);非阻塞循环在一个回调中将所有僵尸进程都全部处理掉
  • waitpid(int pid, int* status, int options); options为WNOHANG表示将waitpid设置为非阻塞,没有子进程退出则立即报错返回。为0默认为阻塞等待子进程退出。返回值:>0 退出的子进程的pid;==0有子进程但是没有退出; <0没有子进程了出错了。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>

void sigcb(signum )
{
	//使所有僵尸进程退出
	while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main()
{
	signal(SIGCHLD, sigcb);
	int pid = fork();
	if(pid == 0)
	{
		sleep(3);
		exit(0);
	}
	
	while(1)
	{
		printf("hello Linux!\n");
		sleep(3);
	}
	
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值