Linux--信号

一:信号

1.基本概念

  • 信号是操作系统通知某个进程有异常或有某种事件发生的通知机制。
    通知机制:是以通知事件发生为目的。
    通信机制:是以传输数据为目的。
    • kill -l查看信号,编号为1-31为普通信号,34-64为实时信号。我们现在只研究普通信号

这里写图片描述
1.信号产生的方式

  • 由硬件组合键产生(ctrl+c(2号信号)、ctrl+(3号信号))–只能发给前台进程。
  • 由硬件异常产生(cpu除0,内存野指针)
  • 命令、接口操作系统产生。
  • 由软件条件产生(sigpipe读端不读还关闭读端,写端一直写)

:不论是什么产生的信号,发送信号最后一定时操作系统,因为操作系统是进程管理者。

2.如何理解操作系统给一个进程发信号?

  • 本质就是操作系统修改了目标进程PCB中的pending表的对应比特位。

3.处理信号

  • 进程不是在收到信号时立即处理信号,而是先在pending表中记录,等到合适的时候再去处理。合适的时候,就是当进程从内核态返回到用户态时,要对信号信息检测并处理。
  • 简单来说,一个进程在执行系统调用时就处于内核态。

4.信号递达的方式

  • 默认
  • 忽略
  • 自定义(信号捕捉):9号信号不能被捕捉。
    这里写图片描述

5.信号在内核中的表示示意图
这里写图片描述

二:信号衍生概念

1.core dump(核心转储):

  • 一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常为core, 这叫做 core dump。系统默认是不允许产生core文件的,因为文件中可能包含用户密码等敏感信息,不安全。但是开发人员可以在开发调试阶段用ulimit命令改变这个限制,允许产生core文件。
    ulimit -a 查看core文件大小
    ulimit -c 1024 改变core文件大小

2.可重入函数:

  • 正在执行main函数执行流的时候,j假设main函数正在调用insert方法(向全局链表中头插节点),节点还没有插入成功,main函数执行流可能-因为中断、异常或者系统调用而从用户态进入内核态,当从内核态返回到用户态时,要对信号信息进行检测和处理,假设这里的处理方式为信号捕捉,那么这里返回至用户态就去执行handler函数,而不是回到主控制流程,这里就可能有两个执行流。假设handler函数也调用了insert方法,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。
  • 一个函数如果符合下面条件之一则不是可重入函数
    (1)调用了malloc或free,因为malloc也是用全局链表来管理堆的。
    (2)调用标准I/O库函数。

3.volatile(保证内存可见性)

  • 因为信号可能会产生多执行流,有可能因为编译器优化,产生数据二意性问题。
    gcc的优化选项:
    这里写图片描述
    这里写图片描述
  • 正常情况,上面的代码在接收到2号信号后会因为信号捕捉,hanler方法里面改变了flag的值导致while循环退出而结束。结果如下:
    这里写图片描述

但是有的编译器会对代码进行优化,因为这里对flag一直在做判断,每次从内存上拿数据太麻烦,所以这里就在寄存器上存了一份数据,当收到2号信号时,只是改变了flag在内存上的值,但是判断读取的值在寄存器上,所以为了保证内存数据的可见性,用volatile。

4.SIGCHLD信号:

  • 子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义。可以用于回收多个子进程。
    这里写图片描述
  • 父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出的子进程子终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程,系统默认的忽略动作和用户用sigaction函数自定义的忽略通常时没有区别的,但是这是一个特例。此方法对于Linux可用。
    这里写图片描述
    5.竞态条件:

  • 由于异步事件在任何时候都有可能发生(这里的异步事件指出现更高优先级的进程),如果我们写程序是考虑不周全,就有可能由于时序问题而导致错误,这叫做竞态条件。

  • 小案列:mysleep
    这里写图片描述
  • main函数为一个执行流,可能因为中断、异常、系统调用等从用户态进入到内核态,在从内核态返回到用户态时,会对信号信息进行检测和处理,这里可能就会因为闹钟时间到了处理闹钟信号,因为闹钟信号处理方式为信号捕捉,所以会从内核态返回到用户态去执行另一个执行流(hanler),在执行完handler后准备返回时,可能会被”切出去”,再回来时因为已经处理完alarm信号,但是又没有被pause收到,所以可能会造成进程永久挂起。这里我们可以在调用pause之前屏蔽SIGALARM信号使它不能提起递达,但是这里必须保证“接触信号屏蔽”和“挂起等待信号(pause)”这两部合并成为一个原子操作。
int sigsuspend(const sigset_t *sigmask):

三:有关函数

1.

  • sighandler_t signal(int signum, sighandler_t handler);//用来捕捉信号,第一个参数为要捕捉的信号编号,第二个参数为函数指针。
  • int kill(pid_t pid, int sig);//给任意进程发送任意信号。
  • int raise(int sig);//给自己发送任意信号
  • unsigned int alarm(unsigned int seconds);//设置一个闹钟,告诉内核在seconds秒忠厚想当前进程发送SIGALRM信号,该信号默认处理动作是终止当前进程。//I/O严重影响程序的性能

2.信号集操作函数

  • int sigemptyset(sigset_t *set);//清空
  • int sigfillset(sigset_t *set);//填充,使其中所有信号对应bit置位,表示该信号集的偶小信号包括系统支持的所有信号。
  • int sigaddset(sigset_t *set, int signum);//添加信号
  • int sigdelset(sigset_t *set, int signum);//删除信号
  • int sigismember(const sigset_t *set, int signum);//判断一个信号是否在

3.sigprocmask

  • 作用:可以读取或更改进程的信号屏蔽字(阻塞信号集)–操控进程block表
  • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);//第一个参数有三个选项,SIG_BLOCK向阻塞信号集中添加信号,SIG_UNBLOCK从当前阻塞信号集中删除信号,SIG_SETMASK设置新的阻塞信号。第二个参数为信号集,第三个参数为保存旧的信号集,不想保存设置为NULL。
  • int sigpending(sigset_t *set);//获取当前进程的未决信号集,通过set参数传出。成功返回0,失败-1.
    修改进程pending表就是发信号。
    这里写图片描述
    这里写图片描述

  • int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);//与signal作用相同,可以读取和修改与指定信号相关联的处理动作。

 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);//
           };

这里写图片描述
- int pause(void);//pause函数使调用进程挂起直到偶信号递达。pause只有出错(信号处理的动作使捕捉)的返回值。

猜你喜欢

转载自blog.csdn.net/virgofarm/article/details/80494513