Linux下的进程信号

深入理解Linux进程信号处理
本文详细介绍了Linux下进程信号的产生方式,包括键盘输入、程序异常和kill命令等。讨论了信号在内核中的表示,如pending、block和hander表,并阐述了进程收到信号后的处理动作,如设置信号屏蔽字、修改信号处理动作。特别地,文章还讲解了SIGCHLD信号及其在子进程退出时的作用。

信号的产生

信号的产生方式

  • 键盘产生

    • 键盘产生的信号只能发送给前台进程。例如:[Ctrl+C]…
  • 程序异常

    • 除0错误。除0错误会导致硬件错误。
    • core dumped(核心转储):当进程异常退出时,操作系统会将该进程发生异常退出之前在内存中的数据存储至硬盘上。
      • 但是通常发生程序异常退出时,用户并不会发现程序产生了一个core dumped文件,这是因为操作系统当前给用户可产生core file size大小为0,因为core文件的大小不是很小的,所以一般系统会将可产生core文件的大小为0,即通常不产生core文件。
      • ulimit -a:查看系统当前分配给用户设置资源方面的限制条件。
      • core文件前面是core,后面是引起产生该core文件的进程pid。
      • 使用core文件调试代码:在Linux环境下,进入使用gdb调试代码,在gdb内部,使用core-file [core文件名],就会直接定位到引发core文件产生的对应行。
      • 2、9号信号不会产生core文件。
  • 使用kill命令

  • 通过系统调用接口给特定进程发送信号

    • #include<signal.h>
      int kill(pid_t pid, int signo);
      //向特定进程发送特定信号;成功返回0;失败返回-1
      
    • #include<signal.h>
      int raise(int signo);
      //向当前进程发送特定信号;成功返回0;失败返回-1
      
    • #include<stdlib.h>
      void abort(void);
      //使当前进程收到信号而异常终止;就像exit()函数一样,abort()函数总是会成功的,所以没有返回值
      
  • 由软件条件发送信号

    • SIGPIPE:SIGPIPE是一种由软件条件产生的信号,当一个管道的读端被关闭时,这时候操作系统就会检测到该管道中写入的数据不会在有人来管道内读文件了,操作系统会认为该管道的存在会造成内存资源的极大浪费,则操作系统就会向写端对应的目标进程发送SIGPIPE信号。

    • #include<unistd.h>
      unsigned int alarm(unsigned int seconds);
      //调用alarm函数可以对当前进程设置一个闹钟,也就是告诉操作系统在seconds秒之后对当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程。
      
      • 返回值:该函数的返回值是0或者是以前设定的闹钟时间还剩余的秒数。如果second的值为0,则表示取消以前设定的闹钟。

信号的在内核中的表示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lyWtWQ2r-1608611148071)(https://s1.ax1x.com/2018/11/19/FSqxRH.png)]

进程的PCB包含了进程的一切信息,所以它理所应当的要保存进程的信号信息。操作系统使用了两张位图&一张函数指针数组表来控制进程的信号

  • 实际执行信号的处理动作称为信号递达。
  • 信号从产生到递达之间的状态,称为信号未决。
  • 被阻塞的信号产生时将保存在未决状态,直到进程解除对该信号的阻塞,才能执行递达的动作。
  • 阻塞和忽略不同,只要信号被阻塞就不会递达;而忽略是在递达之后,可选的一种处理动作。

pending(未决信号表)

  • pending表用来表示进程收到的未决信号。当进程收到一个信号时,就会将该进程PCB中的pending表中对应的表示该信号的位图置为1。当信号被递达时,对应位置会被置为0。
    • 常规信号在递达之前产生多次只记一次,而实时信号在递达之前产生多次可以依次放在一个队列里

block(阻塞信号表)

  • block表用来表示进程的哪些信号被阻塞。当进程的PCB中的block表中的一个信号对应的位图被置为1时,代表该进程阻塞了对应的信号,即当该进程收到对应信号时,对应信号的pending位一直为1,无法被递达。

hander(函数指针数组)

  • 该表对应了相应信号的处理方式,保存了处理信号方式的函数指针。该处保存的是信号的默认处理动作还是用户自定义的处理动作,忽略信号如何定义处理动作?

用户自定义进程收到信号后的处理动作

sigset_t

当需要得知进程的pending表或block表中的信息时,需要定义一个特有的结构来存储进程中的信号信息。sigset_t是专门用来存储进程信号信息的结构,虽然进程中的保存信号的方式是以位图来保存,但这里禁止直接使用位操作来操作sigset_t的数据,必须通过特定的函数接口堆sigset_t进行操作。

  • 当使用sigset_t结构来接收进程的block信号集时,通常称为进程的信号屏蔽字。
  • 当使用sigset_t结构来接收进程的pending信号集时,通常称为进程的pending信号集。

信号集操作函数

#include<signal.h>

int sigemptyset(sigset_t *set);
//初始化set所指向的信号集,使其中所有信号对应的比特位清零,表示该信号集不包含任何信号

int sigfillset(sigset_t *set);
//初始化set所指向的信号集,将其中所有信号对应的比特位置1,表示该信号集的有效信号包括系统支持的所有信号

int sigaddset(sigset_t *set, int signo);
//表示将set所指向的信号集中的signo信号置1

int sigdelset(sigset_t *set, int signo);
//表示将set所指向的信号集中的signo信号清零

int sigismember(const sigset_t *set, int signo);
//用来判断set所指向的信号集的有效信号中是否包含signo信号,包含返回1,不包含返回0,出错返回-1
  • 注意:在使用sigset_t类型的变量前,一定要调用sigemptyset或sigfillset进行初始化,使信号集处于某种确定的状态,初始化之后就可以调用sigaddset或sigdelset在信号集中添加或删除某种有效信号。

设置\修改进程的信号屏蔽字(block表)

#include<signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
  • int how:
    • SIG_BLOCK:set包含了用户希望添加到当前信号屏蔽字的信号,即就是在老的信号屏蔽字中添加上新的信号。相当于:mask=mask|set
    • SIG_UNBLOCK:set包含了用户希望从当前信号屏蔽字中解除阻塞的信号,即就是在老的信号屏蔽字中取消set表中的信号。相当于:mask=mask&~set
    • SIG_SETMASK:设置当前进程的信号屏蔽字为set所指向的信号集。相当于:mask=set
  • const sigset_t *set:将要设置为进程block表的信号集。
  • sigset_t *oset:用来保存进程旧的block表。
    • 若无需保存进程旧的block表,传递空指针即可。

获取进程的pending信号集

#include<signal.h>

int sigpending(sigset_t *set);
  • 成功返回0;失败返回-1

修改进程收到信号的处理动作

进程收到信号的处理方式
  • 忽略
    • SIG_IGN:进程收到信号忽略的处理方式。
  • 默认处理方式
    • SIG_DFL:进程收到信号的默认处理方式。
  • 自定义处理方式
sigaction
#include<signal.h>

struct sigaction
{
    void (*sa_handler)(int);//指向信号处理对应的函数
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;//当在处理所收到信号时,想要附带屏蔽的其他普通信号,当不需要屏蔽其他信号时,需要使用sigemptyset初始化sa_mask
    int sa_flags;
    void (*sa_restorer)(void);
};

int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • int signo:指定的信号编号。
  • const struct sigaction *act:若该act指针非空,则根据act指针来修改进程收到signo信号的处理动作。
  • struct sigaction *oact:若oact指针非空,则使用oact来保存信号旧的处理动作。

信号的捕捉

FSLGYF.png

处理信号的时机

进程收到一个信号时,并不会立即就去处理这个信号,而是先将收到的信号保存下来,并在合适的时候对信号进行处理,操作系统会在进程进入了内核态并从内核态返回用户态时,检测进程中可以进行处理的信号,并进行处理

用户写好的代码会在什么情况下进入内核态呢?
  • 调用系统调用接口
  • 异常
  • 中断

SIGCHLD信号

使用fork函数创建出一个子进程,当子进程退出时会向父进程发送一个SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理方式。

代码验证子进程退出时,向父进程发送SIGCHLD信号
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<sys/types.h>

//在屏幕模拟打印出进程的pending表
void showpending(const sigset_t* set)
{
    size_t i = 0;
    for(i = 0; i < 32; ++i)
    {
        if(sigismember(set, i))
            printf("1");
        else
            printf("0");
    }
    printf("\n");
}

int main()
{
    pid_t id = fork();
    if(-1 == id)//创建子进程失败
        exit(-1);
    else if(0 == id)//子进程
    {
        int count = 5;
        while(count--)//子进程约五秒后退出
        {
            printf("i am child...\n");
            sleep(1);
        }
        printf("i quit...\n");
    }
    else
    {
 		//因为子进程收到SIGCHLD信号的默认处理方式是忽略,所以我们需要屏蔽SIGCHLD信号,使父进程收到SIGCHLD信号之后,SIGCHLD信号会一直保存在父进程的pending表中
        sigset_t set;//用来接受父进程的pending表
        sigset_t newblock;//用来屏蔽父进程的SIGCHLD信号
        sigemptyset(&set);
        sigemptyset(&newblock);
        sigaddset(&newblock, SIGCHLD);
        sigprocmask(SIG_SETMASK, &newblock, NULL);
        while(1)
        {
            sigpending(&set);
            showpending(&set);
            sleep(1);
        }
    }
}

//该程序仅供测试使用,父进程未进行回收子进程资源

运行结果:F9JMpq.png

完整虚拟存储管理器实验报告!一、实验目的请求页式虚存管理是常用的虚拟存储管理方案之一。通过请求页式虚存管理中对页面置换算法的模拟,有助于理解虚拟存储技术的特点,并加深对请求页式虚存管理的页面调度算法的理解。二、实验环境 Turbo C 2.0/3.0或VC++6.0三、实验内容本实验要求使用C语言编程模拟一个拥有若干个虚页的进程在给定的若干个实页中运行、并在缺页中断发生时分别使用FIFO和LRU算法进行页面置换的情形。其中虚页的个数可以事先给定(例如10个),对这些虚页访问的页地址流(其长度可以事先给定,例如20次虚页访问)可以由程序随机产生,也可以事先保存在文件中。要求程序运行时屏幕能显示出置换过程中的状态信息并输出访问结束时的页面命中率。程序应允许通过为该进程分配不同的实页数,来比较两种置换算法的稳定性。四、实验说明 1.设计中虚页和实页的表示本设计利用C语言的结构体来描述虚页和实页的结构。pnpfntimepnpfnnext 虚页结构 实页结构在虚页结构中,pn代表虚页号,因为共10个虚页,所以pn的取值范围是0—9。pfn代表实页号,当一虚页未装入实页时,此项值为-1;当该虚页已装入某一实页时,此项值为所装入的实页的实页号pfn。time项在FIFO算法中不使用,在LRU中用来存放对该虚页的最近访问时间。在实页结构中,pn代表虚页号,表示pn所代表的虚页目前正放在此实页中。pfn代表实页号,取值范围(0—n-1)由动态指派的实页数n所决定。next是一个指向实页结构体的指针,用于多个实页以链表形式组织起来,关于实页链表的组织详见下面第4点。2.关于缺页次数的统计为计算命中率,需要统计在20次的虚页访问中命中的次数。为此,程序应设置一个计数器count,来统计虚页命中发生的次数。每当所访问的虚页的pfn项值不为-1,表示此虚页已被装入某实页内,此虚页被命中,count加1。最终命中率=count/20*100%。3.LRU算法中“最近最久未用”页面的确定为了能找到“最近最久未用”的虚页面,程序中可引入一个时间计数器countime,每当要访问一个虚页面时,countime的值加1,然后将所要访问的虚页的time项值设置为增值后的当前countime值,表示该虚页的最后一次被访问时间。当LRU算法需要置换时,从所有已分配实页的虚页中找出time值为最小的虚页就是“最近最久未用”的虚页面,应该将它置换出去。4.算法中实页的组织因为能分配的实页数n是在程序运行时由用户动态指派的,所以应使用链表组织动态产生的多个实页。为了调度算法实现的方便,可以考虑引入free和busy两个链表:free链表用于组织未分配出去的实页,首指针为free_head,初始时n个实页都处于free链表中;busy链表用于组织已分配出去的实页,首指针为busy_head,尾指针为busy_tail,初始值都为null。当所要访问的一个虚页不在实页中时,将产生缺页中断。此时若free链表不为空,就取下链表首指针所指的实页,并分配给该虚页。若free链表为空,则说明n个实页已全部分配出去,此时应进行页面置换:对于FIFO算法要将busy_head 所指的实页从busy链表中取下,分配给该虚页,然后再将该实页插入到busy链表尾部;对于LRU算法则要从所有已分配实页的虚页中找出time值为最小的虚页,将该虚页从装载它的那个实页中置换出去,并在该实页中装入当前正要访问的虚页。~
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值