Linux之信号机制

1. 什么是信号

1.1 信号的特点

  • 简单
  • 不能携带大量信息
  • 满足某个特设条件才会给进程发送信号
  • 信号是通过软件方法实现的(时钟中断是通过硬件实现的),其实现手段导致信号有很强的延时性,但对用户来说很难感知(软中断
  • 所有进程收到的信号,都是由内核负责发送的,也由内核处理
  • 进程A给进程B发信号其实本质是进程A驱使内核给进程B发信号。

补充:软中断不如硬中断靠谱与及时,硬中断能精确控制中断的时间,但是软中断可能会有延时。因为软件运行时间有随机性

1.2 与信号相关的事件和状态

  • 产生信号的事件
    • 按键产生:如ctrl+c,ctrl+z
    • 系统调用产生:如kill、raise、abort
    • 软件产生:如定时器alarm
    • 硬件异常产生:如非法访问内存、除0、内存对其出错
    • 命令产生:kill
  • 递达状态
    • 信号已经被进程接收时的状态。
  • 未决状态
    • 信号从产生到递达之间的状态
  • 阻塞状态
    • 如果没有阻塞状态,未决状态几乎立马就会变成递达状态
    • 因为有了阻塞状态,会阻止未决状态变为递达状态。
  • 信号处理方式:
    • 执行默认动作
    • 忽略(丢弃)
    • 捕捉(调用用户处理函数)
  • 未决信号集(pending)
    • 是BItSet类型(位图)
    • 信号存在则将相应位置为1,不存在则置为0
  • 阻塞信号集(block)
    • 是BitSet类型(位图)
    • 信号被阻塞就将相应位置为1,否则就置为0.

 1.3 信号4要素

  • 编号(1~64)
  • 名称
  • 事件
  • 默认处理动作
    • Term:终止进程
    • Ign:忽略信号
    • Core:终止进程,生成Core文件(可以用于检查死亡原因,用于gdb调试)
    • Stop:停止(暂停)进程
    • Cont:继续运行进程

1.4 信号相关补充

  • 查看linux中的信号
man -7 signal

kill -l
  • 9号信号(SIGKILL)和19号信号(SIGSTOP)不允许捕捉忽略,只能执行默认动作

2. 信号的产生

2.1 终端按键产生信号

  • ctrl+c:产生2号信号(SIGINT) ,中断进程
  • ctrl+z:产生成20号信号(SINTSTP),暂停与终端交互进程的运行(后台挂起)
  • ctrl+\: 产生3号信号(SINQUIT),退出进程

2.2 硬件异常产生的信号

  • 除0错误:产生8号信号(SIGFPE),浮点错误
  • 非法访问内存:产生11号信号(SIGEGV),段错误
  • 总线错误:产生7号信号(SIGBUS)

2.3 kill函数/命令产生信号

  • 命令模式
# kill 信号 进程pid
kill -19 19665
  • kill函数
int kill(pid_t pid, int sig);
  • 参数pid
    • pid>0:表示进程的id,发送信号给指定进程
    • pid=0:发送信号给与调用kill进程属于同一个进程组的所有进程
    • pid<0:取|pid|,发给对应的进程组
    • pid=-1:发送给当前进程有权限发送的系统中的所有进程
  • 补充
    • 进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组的id与进程组长的id相同。
    • 权限保护:super用户可以向任意用户发送信号,而不普通用户不能向系统用户发送信号。普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。

2.4 raise函数和abort函数产生信号

  • raise函数:自己给自己发信号
int raise(int sig);
  • abort函数:给自己发异常终止信号(6,SIGABRT)
void abort(void);

2.5 软件条件产生信号

  • alarm函数:指定时间后内核给当前进程发14号信号(SIGALRM,默认动作终止进程)
unsigned int alarm(unsigned int seconds);
  • 每一个进程有且仅有一个定时器
  • 定时器的计时与进程所处状态无关。
  • 返回值:上一次定时没有定时完的剩余时间
  • setitimer函数:也是定时器,更精确,可以周期性定时
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
  • 返回值:是否成功
  • which:
    • ITIMER_REAL:自然定时,计算自然时间,会发送14号信号(SIGLARM)
    • ITIMER_VIRTUAL:虚拟空间计时(用户空间),只计算进程占用CPU时间,会发送26号信号(SIGVTALRM)
    • ITIMER_PROF:运行时计时(用户+内核),计算占用cpu以及执行系统调用的时间,会发送27号信号(SIGPROF)
  • new_value:传入参数,结构体
  • old_value:传出参数,结构体

3. 信号相关的操作

3.1 信号集相关操作

  • 将某个信号集全部清0
int sigemptyset(sigset_t *set);

        返回值:0代表成功,-1代表失败        

        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);
  • 返回值
    • 0:不在
    • 1:在
    • -1:出错

3.2 sigprocmask函数

  • 用来屏蔽信号、接触屏蔽。
  • 屏蔽信号是将信号处理延后执行;而忽略表示对信号的处理为丢弃
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 返回值:是否成功
  • how:有三个取值(假设当前的信号屏蔽字为mask)
    • SIG_BLOCK:表示set参数代表需要屏蔽的信号。相当于mask=mask|set
    • SIG_UNBLOCK:表示set参数代表需要解除屏蔽的信号
    • SIG_SETMASK:表示用set参数替换原先的mask
  • set:传入参数,是个位图。set中哪一位为1就表示当前进程要屏蔽哪个信号
  • oldset:传出参数,保存旧的信号屏蔽集。

3.3 sigpending函数

  • 读取当前进程的未决信号集
int sigpending(sigset_t *set);
  • 返回值:是否成功
  • set:传出参数

3.4 signal函数:给信号注册捕捉函数

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • 定义一个函数指针,返回值类型为void,参数类型为int
  • 内核会调用自定义动作。
  • 该函数不推荐使用

3.5 sigaction函数:linux中给信号注册一个捕捉函数

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 
  • 返回值:是否成功
  • act:传入参数
  • oldack:传出参数
struct sigaction {
        void     (*sa_handler)(int);
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t   sa_mask; 
        int       sa_flags; 
        void     (*sa_restorer)(void);
};
  • sa_handler:指定信号捕捉后的处理函数名
  • sa_mask:CPU在执行信号处理函数期间,所要屏蔽的信号集合
  • sa_flags:通常设置为0,表使用默认属性。
    • 0:表示 表使用默认属性
    • SA_SIGINFO:选用sa_sigaction来指定捕捉函数
    • SA_INTERRURT:慢系统调用被信号中断后,不重启。
    • SA_RESTART:慢系统调用被信号中断后,重启慢系统调用
    • SA_DEFER:不自动屏蔽本信号。
  • sa_sigaction:指定带参数的信号捕捉函数。

3.6 总结:内核实现信号捕捉过程

  1. 进程A在执行控制流程的某条指令时,收到信号,要进行中断、异常处理或系统调用进入内核
  2. 内核处理完异常,准备回用户模式之前,先处理当前进程中可以递送的信号。
  3. 如果信号的处理动作是自定义信号处理函数,则回到用户态执行信号处理函数(但不是回到主控制流程)
  4. 信号处理函数返回时会执行特殊的系统调用再次进入内核,
  5. 内核再返回到用户模式,回到从主控制流程中上次被中断的地方继续执行。

4. 竞态条件

4.1pause函数:进程主动挂起自己

  • 调用该函数可以造成进程主动挂起自己,等待信号唤醒。
  • 调用该系统调用后,进程将处于阻塞状态,直到有信号递达将其唤醒才会有返回值。
int pause(void);
  •  返回值: -1,并设置errno为EINTR
    • 如果信号的默认处理动作是终止进程,则进程终止,pause函数有机会返回。
    • 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回
    • 如果信号的处理动作是捕捉,则调用完信号处理函数后,pause返回-1。并将errno设置为EINTR,表示“被信号中断”
    • pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒

4.2 时序问题小案例

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

/*所有信号处理函数的原型,都类此,
 *无返回值(void),只有一个参数,表示信号编号*/
void sig_alrm(int signo)
{
	/*用来占位,可以不做任何事,但这个函数存在
	 *SIGALRM信号
	 *就不执行默认动作终止进程,而做其它事情*/
}

unsigned int mysleep(unsigned int sec)
{
	struct sigaction act, old;
	unsigned int unslept;			//保存未休眠够的时间

	act.sa_handler = sig_alrm;
	sigemptyset(&act.sa_mask);		//清空
	act.sa_flags = 0;
	sigaction(SIGALRM, &act, &old);	//注册信号处理函数sig_alrm
									//同时要保存旧的处理方式

	alarm(sec);						//设置sec秒闹钟
    
    /*如果在此时,进程失去cpu控制权,然后5秒钟也过去了,就会给进程发一个信号,
     *等之后再获得cpu也会先执行信号处理。则就会产生严重的bug,被pause的进程
     *永远不会被唤醒。
        */
	pause();			//进程阻塞,收到一个信号后,pause返回-1,解除阻塞

	unslept = alarm(0);	//取消旧的定时器,将剩余时间保存

	sigaction(SIGALRM, &old, NULL);	//恢复SIGALRM信号原来的处理方式

	return unslept;
}

int main(void)
{
	while(1){
		mysleep(5);
		printf("Five seconds passed\n");
	}

	return 0;
}
  •    如何解决时序问题带来的bug

4.3 sigsuspend函数

  • 对于时序要求严格的场合一般用sigsuspend函数代替pause
int sigsuspend(const sigset_t *mask);
  • 功能:也是挂起进程,等待信号。
  • 参数mask:在执行这个语句的期间,进程的屏蔽字集合由mast决定。执行完这个语句之后,进程的屏蔽字再换回进程原本的屏蔽字集合。
#include <unistd.h>
#include <signal.h>
#include <stdio.h>

void sig_alrm(int signo)
{
    /* nothing to do */
}

unsigned int mysleep(unsigned int nsecs)
{
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    //1.为SIGALRM设置捕捉函数,一个空函数
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    //2.设置阻塞信号集,阻塞SIGALRM信号
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);   //信号屏蔽字 mask

    //3.定时n秒,到时后可以产生SIGALRM信号
    alarm(nsecs);

    /*4.构造一个调用sigsuspend临时有效的阻塞信号集,
     *  在临时阻塞信号集里解除SIGALRM的阻塞*/
    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);

    /*5.sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集
     *  这个信号集中不包含SIGALRM信号,同时挂起等待,
     *  当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集*/
    sigsuspend(&suspmask); 

    unslept = alarm(0);
    //6.恢复SIGALRM原有的处理动作,呼应前面注释1
    sigaction(SIGALRM, &oldact, NULL);

    //7.解除对SIGALRM的阻塞,呼应前面注释2
    sigprocmask(SIG_SETMASK, &oldmask, NULL);

    return(unslept);
}

int main(void)
{
    while(1){
        mysleep(2);
        printf("Two seconds passed\n");
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值