• Linux:信号



    全文约 3036 字,预计阅读时长: 9分钟


    信号

    • 信号是进程之间事件异步通知的一种方式,属于软中断。过程:信号产生,信号识别,处理处理。
    • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2
      • Ctrl-C 产生的信号只能发给前台进程。 Shell可以同时运行一个前台进程和任意多个后台进程,
      • 一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
    • 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
    • 对于相当一部分信号而言,当进程收到的时候,默认的处理动作是终止当前进程。
      • 对进程而言,有一些信号不能被捕捉和忽略,如: kill -9
      • 进程收到信号,不是立即处理的,而是在合适的时候。
    • 信号的处理:
      • 默认方式(部分终止进程,部分有特定的功能)
      • 忽略信号
      • 自定义方式:捕捉信号
    • 站在语言角度:程序崩溃;站在系统角度,进程受到了信号。

    信号发送

    • 信号的产生有如下的方式:
      • kill 命令产生 kill -l 1—31普通信号;34—64实时信号,响应要求级别特别强的信号,一旦发出进程必须响应。
      • 键盘产生
      • 由软件条件产生信号:闹钟
      • 程序异常 、硬件异常产生
        • 当你的进程触发错误时,基本都有对应的软硬件监控,cpu下的状态寄存器,内存和页表mmu等,会被OS识别到,然后给目标进程发送信号,来达到终止进程的目的。
    • 信号的产生,在进程的运行的任何时间点都可以产生,有可能进程正在做更重要的事情。
      • 因为信号不是立即处理的,所以信号在进程的PCB里保存着。
      • 对进程而言:是否有信号、是什么信号
        • 存储方式:位图,无符号整形,1在比特位中的位置意味着是哪个信号,有没有1意味着有没有信号。
      • 是谁发的,如何发;直接简介通过OS向进程发信号。
        • 发送信号的本质,相当于写对应进程的PCB的位图。因为OS是进程的管理者,OS是由这个能力和义务的。
    • 由软件条件产生信号:闹钟定时终止
    #include 
    unsigned int alarm(unsigned int seconds);
    //调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
    ----//一秒内可以加多少次。
    int a =0;
    void add(int signo)
    {
    	cout<<signo<<endl;
    	cout<<a<<endl;
    	exit(1);
    }
    int main()
    {
    	for(int i =1,i<32;i++)
    	{
    		signal(i,add);
    	}
    	alarm(1);
    	while(true)
    	{
    		a++;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 系统调用产生信号:
    void abort(void;
    int raise(int sig);
    int kill(pid_t pid, int sig);
    
    ---//kill测试  kill 9 1231231
    int main(int argc,char* argv[])
    {
    	kill(atoi(argv[2]),atoi(argv[1]));
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • MakeFile
    CC=g++
    LDFLAGS=-std=c++11 -g  //标准
    Src=mysignal.cc  
    Bin=mysignal
    
    $(Bin):$(Src)
    	$(CC) -o $@ $^ $(LDFLAGS)
    
    .PHONY:clean
    clean:
    	rm -f $(Bin)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    core dump文件

    • 核心转储,OS将进程运行时的核心数据dump到磁盘上,方便用户调试使用。快速定位BUG,标注了错误出现在了哪一行。
    • 一般而言,核心转储是关闭的。自己写出来的错误才会有core dump 标志位设置,不是所有的信号都设置
    ulimit -a
    ulimit -c 1024 //打开core转储开关生成文件
    gdb mytest
    gdb core-file core.....
    
    • 1
    • 2
    • 3
    • 4
    • 查看core dump标志位是否设置
    ---//子进程崩溃 父进程收集
    int main()
    {
    	...
    	int s=0;
    	pid_t  ret = waitpid(-1,&s,0);
    	if(ret>0)
    	{
    		cout<<((s>>7)&0x1)<<endl;
    	}
    0x7F7个高电平,1个低电平 0111 1111
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    信号设置

    在这里插入图片描述

    • bolock 位图:代表是否哪种信号阻塞(屏蔽)
    • pending 位图表示:有哪种未决信号
    • sighandler数组表示:对应信号的处理方式(递达)。
      • 默认(终止等)
      • 忽略
      • 自定义捕捉,由用户提供。
    • sigset_t系统提供的数据类型,用来存储或设置位图中的信号,称为信号集。
    • 修改设置位图中的标志位,需要一系列系统提供的信号集操作函数
    #include 
    int sigemptyset(sigset_t *set);  ---初始化,位图中标志全部请0
    int sigfillset(sigset_t *set);   --全部置1
    int sigaddset (sigset_t *set, int signo);	---指定位置设置1
    int sigdelset(sigset_t *set, int signo);	---指定位置设置0
    int sigismember(const sigset_t *set, int signo);   ---判断特定信号是否被设置
    
    ----都是成功返回0,出错返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • sigprocmask设置阻塞信号集
      • int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
      • 参数:oset:非空指针,返回修改之前的信号屏蔽字。 set:非空指针,则更改进程的信号屏蔽字;
      • how:指示如何更改,参数的可选值:
        • SIG_BLOCK:将set信号添加到阻塞位图中。
        • SIG_UNBLOCK:解除阻塞信号set
        • SIG_SETMAXK:将当前信号屏蔽字设置成 set 信号。
        • 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
    • sigpending读取当前进程的未决信号集,通过set参数传出int sigpending(sigset_t *set);
    1. 阻塞 2号信号
    2. 不断获取pending信号集,并打印
    3. 发送2号信号给进程
    4. 过一段时间,解除对2号信号的阻塞
    5. 2号信号立马会被递达,执行默认动作。
    6. 依旧打印pending未决信号集
    void show_pending(sigset_t *pending)
    {
        for(int i = 1; i <= 31; i++){
            if(sigismember(pending, i)){	//判断信号在不在集合里
                cout << "1";
            }
            else{
                cout <<"0";
            }
        }
        cout << endl;
    }
    
    int main()
    {
        sigset_t in;
        sigemptyset(&in);
        sigaddset(&in, 2); //set 2 signo block, user stack
        sigprocmask(SIG_SETMASK, &in, NULL); //kernel 2 block
        int count = 0;
        sigset_t pending;
        while(true){
            sigpending(&pending);
            show_pending(&pending);
            sleep(1);
            if(count == 20){
                sigprocmask(SIG_SETMASK, NULL, &in); //恢复之后,2号信号立马递达,并且执行默认动作!!!
                cout << "my: ";
                show_pending(&in);
                cout << "recover default: ";
                show_pending(&out);
            }
            count++;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • Linux下:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

    信号处理

    • 进行信号递达的时间:从内核态返回用户态时,尝试信号检测与捕捉执行。
    • 内核态与用户态:
        进程的地址空间0-3G是用户空间,3-4G是内核空间;操作系统提供的系统调用接口内核空间中,而内核空间中的代码数据在物理内存上放着,因此有一个内核级页表维护这个映射关系;再由于进程有多个,操作系统只有一个,因此内核级页表只有一个,且是共享的。
        用户自己的代码数据,通过接口访问内核代码数据,系统会自动进行身份切换,进入内核空间进行一系列操作。此时进程在用户空间的状态就叫用户态,在内核空间的状态叫做内核态。CPU中存在一个与 权限相关的寄存器数据标识所处的状态。
    • 故操作系统设计时:OS从内核态切换至用户态,会检测信号集是否需要被处理。
      在这里插入图片描述
    • 当去执行自定义信号捕捉的方法时,是需要切换至用户态的。因为内核态权限时很高的,如果此时有人利用这个bug会去进行大量危险的操作,进行程序替换等,破坏系统或用户的数据等。
    • 递达的处理方法一般有三种:
      • 默认(大部分终止进程)
      • 忽略
      • 自定义信号捕捉:signal()sigaction
    • signal:捕获进程递达的信号,进行怎样的处理
      • void (*signal(int sig, void (*func)(int)))(int)
      • 参数:sig – 在信号处理程序中作为变量使用的常量信号码,有些特定选项(异常终止、除0或算术溢出、野指针等)
        • SIGINT:中断信号常用,由用户产生;也就是kill -l列表里的 1 — 31 的普通信号。
      • func – 一个指向函数的指针,也可以是下面预定义函数之一:
        • SIG_DFL :默认的信号处理程序,大部分默认终止进程。
        • SIG_IGN :忽视信号。
    void handler(int signo)
    {
        std::cout << "get a signal: " << signo << std::endl;
        exit(0);
    }
    int main()
    {
        for(int i=1; i < 32; i++){
            signal(i, handler);//会调用handler函数进行信号处理。
            
            //signal(i, SIG_IGN); ///SIG_ING 代表捕获到忽略 i 信号
       }
    .....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • sigaction:你想捕获哪一个信号,结构体:你想怎么处理这个信号,返回老的信号捕捉方法。
      • int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
        • signo是指定信号的编号。
        • act指针非空,则根据 ac t修改该信号的处理动作。
        • oact指针非 空,则通过oact传出该信号原来的处理动作,不需要可以设置为NULL。
    • act和oact指向sigaction结构体:
     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);//0
               };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    ---//使用
    struct sigaction s1;
    s1.sa_handler = handler;
    s1.sa_flags =0;
    sigemptyset(s1.sa_mask);
    s1.sa_sigaction=NULL;
    S1.sa_restorer = NULL;
    sigaction(SIGINT,&act,NULL);
    void handler(int signo).....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可重入函数

    • 当前运行进程收到信号的处理方法,而此时进程收到信号进行递达处理,之前正在运行的函数栈帧销毁,造成资源泄露等。
    • 一个函数符合以下条件之一则是不可重入的:
      • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
      • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

    volatile

    1. 告诉编译器不要将内存中的变量优化到cpu的寄存器中,cpu找数据时,去内存里找。解决寄存器和内存数据不一致的问题。
    2. 保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作.
    3. 使用:volatile int flag = 0;

    SIGCHLD信号

    • waitwaitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。两种方式各有缺点。
    • 其实子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,调用wait清理子进程即可。
    #include 
    #include 
    #include 
    void handler(int sig)
    {
    	 pid_t id;
    	 while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
    	 printf("wait child success: %d\n", id);
    }
    	 printf("child is quit! %d\n", getpid());
    }
    int main()
    {
    	 signal(SIGCHLD, handler);
    		 pid_t cid;
    		 if((cid = fork()) == 0){//child
    		 printf("child : %d\n", getpid());
    		 sleep(3);
    		 exit(1);
    	 }
     while(1){
    	 	printf("father proc is doing some thing!\n");
    		sleep(1);//在休眠时会被提前唤醒。
    	 }
     return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 是否需要wait子进程:
      • 僵尸进程的内存泄漏
      • 是否需要获得子进程的退出码:不关心就算了。
  • 相关阅读:
    蓝桥每日一题(day2 暴力)扫雷 easy
    【JavaScript】制作一个抢红包雨页面
    k8s使用ECK(2.4)形式部署elasticsearch+kibana-http协议
    WeCross应用搭建整理,遇到的一些问题,WeCross如何搭建?
    vue+element-ui实现主题切换
    redis缓存穿透问题及解决方案代码实现
    Calico IP In IP模拟组网
    千梦网创:你现在赚的钱是三年前选择的结果
    基于Java毕业设计幼儿校园通系统的设计与实现源码+系统+mysql+lw文档+部署软件
    云原生优缺点分析
  • 原文地址:https://blog.csdn.net/WTFamer/article/details/126129603