【linux】进程信号

一、信号相关概念

1、信号的结论

1)进程必须能够识别并处理信号,即使信号没有产生,也要具备处理信号的能力,这种能力属于进程内置功能的一部分。

2)即使进程没有收到信号,也能知道信号的处理方法。

3)当进程收到一个具体的信号时,进程可能不会立即处理这个信号,等到合适的时间才会处理这个信号,信号的处理方式有三种,分别是默认动作,忽略,自定义动作(信号的捕捉)。

4)从信号产生,到信号开始被处理,一定会有时间窗口,进程具有临时保存信号已经发生了的能力。

注意:并不是所有的信号都能被捕获。

eg:SIGKILL(9)和SIGSTOP(19)

2、ctrl + c 为什么能够杀掉我们的前台进程呢?

Linux中,一次登录中,一个终端一般会配上一个bash,每一个登录,只允许一个进程是前台进程(前台进程用来获取键盘输入,键盘输入首先是被前台进程收到的),可以允许多个进程是后台进程。

当一个进程执行时,该进程就变成了前台进程,而bash变成了后台进程。

1~31号信号为普通信号,34~64号信号为实时信号。

ctrl + c 本质是被进程解释成为收到了信号(2号信号)。

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

//int: 收到了哪一个信号
void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
}

int main()
{
    signal(SIGINT, myhandler);//只需设置一次,一直有效!
    //信号的产生和我们自己的代码运行时是异步的

    while(true)
    {
        cout << "I am a crazy process : " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

运行结果:

ctrl + c 的默认动作是终止进程,现在我们设置了自定义动作就会按照我们的要求来执行,但是此时进程就不会终止了,可以通过 kill -9 45836杀掉该进程。

如何将进程变成后台进程呢?

ctrl + c 只能杀掉前台进程,无法杀掉后台进程。

3、键盘数据是如何输入给内核的,ctrl + c 又是如何变成信号的?

键盘摁下ctrl + c,产生硬件中断,OS捕获中断,将其转换为SIGINT信号,并发送给当前前台进程。

二、信号的产生

1、键盘组合键

ctrl + c : 2号信号

ctrl + \ : 3号信号

2、kill 命令

kill -signo pid

3、系统调用

1)kill

可以给任意进程发信号。

代码(自主实现kill命令):

mykill.cc:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " signum pid\n\n";
}

// ./mykill signum pid
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int signum = stoi(argv[1]);
    pid_t pid = stoi(argv[2]);

    int n = kill(pid, signum);
    if(n == -1)
    {

        perror("kill");
        exit(2);
    }

    return 0;
}

proc.c:

复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    while(1)
    {
        printf("I am a process, pid: %d\n", getpid());
        sleep(1);
    }
    return 0;
}

运行结果:

终端1:

终端2:

2)raise

给当前进程发信号。

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

int main()
{
    int cnt = 0;
    while(true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
        cnt++;
        if(cnt % 2 == 0)
        {
            raise(2);
        }
    }
    return 0;
}

运行结果:

3)abort

向进程自身发送SIGABRT(6号)信号,让当前进程异常终止。

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

int main()
{
    int cnt = 0;
    while(true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
        cnt++;
        if(cnt % 2 == 0)
        {
            abort();
        }
    }
    return 0;
}

运行结果:

怎么知道abort发送的是6号信号呢?

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
}

int main()
{
    signal(SIGABRT, myhandler);//执行只是注册信号处理函数,当收到信号时才调用函数

    int cnt = 0;
    while(true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
        cnt++;
        if(cnt % 2 == 0)
        {
            //kill(getpid(), 2);
            //raise(2);
            //abort();
        }
    }
    return 0;
}

运行结果:

终端1:

终端2:

4、异常

当发生除零或野指针时,会让进程崩溃,操作系统会给进程发送信号。

操作系统是硬件的管理者,CPU也是硬件,CPU中有状态寄存器,当发生除零时,状态寄存器中的溢出标志位会由0变成1,虽然我们修改的是CPU内部的状态寄存器,但是这只影响当前进程,操作系统会通过调度机制,让其他进程继续执行。当访问野指针时,因为CPU访问的是虚拟地址,MMU(内存管理单元)通过页表将虚拟地址转换为物理地址,但访问野指针会导致地址转换失败。

代码:

复制代码
#include <iostream>

using namespace std;

int main()
{
    // int a = 10;
    // int b = 0;
    // a /= b;
    // cout << "a = " << a << endl;
    int *p = nullptr;
    *p = 100;

    return 0;
}

运行结果:

5、软件条件

异常不只会由硬件产生,还可以由软件产生。

1)闹钟:SIGALRM(14号信号)

返回值:如果之前没设置过闹钟,或之前的闹钟已经超时,则返回0;

如果之前还有未超时的闹钟,则返回距离上次闹钟剩余的秒数。

代码:

复制代码
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    alarm(3);//3秒后发送信号
    while(true)
    {
        cout << "process is running" << endl;
        sleep(1);
    }
    return 0;
}

运行结果:

2)当一个进程向一个没有读端的管道写入数据时,操作系统会向该进程发送SIGPIPE信号。

代码:

复制代码
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    char buffer[1024];
    int n = 1024;
    n = read(4, buffer, n);
    printf("n = %d\n", n);
    perror("read");
    return 0;
}

运行结果:

6、core dump 标志

默认云服务器上面的core功能是被关闭的。

代码:

复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 500;
        while(cnt)
        {
            cout << "i am a child process, pid: " << getpid() << "cnt: " << cnt << endl;
            sleep(1);
            cnt--;
        }
        exit(0);
    }

    //father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id)
    {
        cout << "child quit info, rid: " << rid << " exit code: " <<
        ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) <<
        " core dump: " << ((status>>7)&1) << endl;
    }
    return 0;
}

运行结果:

打开系统的core dump功能(临时生效),一旦进程出异常,OS会将进程在内存中的运行信息(定位到出错行),给dump(转储)到进程的当前目录,形成core.pid文件(磁盘):核心转储(core dump)

先运行,再core-file:事后调试

7、什么是信号的发送?

对于普通信号而言,信号是给进程的PCB发的。

复制代码
task_struct
{
    int signal;// 0000 0000 0000 0000 0000 0000 0000 0010 普通信号,位图管理信号
    ...           
}
//1.比特位的内容是0还是1,表明是否收到信号
//2.比特位的位置(第几个,不算0),表示信号的编号
//3.所谓的"发信号",本质就是OS去修改task_struct的信号位图对应的比特位
//4.OS是进程的管理者,只有它有资格去修改task_struct内部的属性

三、信号的保存

1、信号其他相关概念

1)实际执行信号的处理动作称为信号递达(Delivery)。

2)信号从产生到递达之间的状态,称为信号未决(Pending)。

3)进程可以选择阻塞(Block)某个信号。

4)被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

5)阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2、在内核中的表示

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。

常规信号在递达之前产生多次信号只计一次,而实时信号在递达之前产生多次信号可以依次放在一个队列里。

3、sigset_t

sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞(1表示阻塞),而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。

4、信号集操作函数

复制代码
#include <signal.h>
int sigemptyset(sigset_t *set);
//初始化set所指向的信号集,将其中所有信号对应的位都清零

int sigfillset(sigset_t *set);
//初始化set所指向的信号集,将其中所有信号对应的位都置1

int sigaddset (sigset_t *set, int signo);
//将指定信号signo添加到信号集set中,即把该信号对应的位置1

int sigdelset(sigset_t *set, int signo);
//从信号集set中移除指定信号signo,即把该信号对应的位清零

int sigismember(const sigset_t *set, int signo);
//检查信号signo是否在信号集set中,即判断该信号对应的位是否为1

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//可以读取或更改进程的信号屏蔽字

how:

set:

为NULL时,不修改屏蔽字,仅读取旧值到oldset。

oldset:

输出型参数:保存修改前的信号屏蔽字。

为NULL时,不保存旧值。

复制代码
#include <signal.h>
int sigpending(sigset_t *set);
//用于获取已经发给进程,但被阻塞住还没处理的信号

代码示例:

复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

void PrintPending(sigset_t &pending)
{
    for(int signo = 31; signo >= 1; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << "\n\n";
}

void handler(int signo)
{
    cout << "catch a signo: " << signo << endl;
}

int main()
{
    // 0. 对2号信号进行自定义捕捉
    signal(2, handler);
    // 1. 对2号信号进行屏蔽 --- 数据预备
    sigset_t bset, oset;//在栈上开辟的空间,属于用户区
    sigemptyset(&bset);
    sigemptyset(&oset);
    sigaddset(&bset, 2);
    // 1.1 调用系统调用,将数据设置进内核
    sigprocmask(SIG_SETMASK, &bset, &oset);//屏蔽2号信号
    // 2. 重复打印当前进程的pending
    sigset_t pending;
    int cnt = 0;
    while(true)
    {
        // 2.1 获取
        int n = sigpending(&pending);
        if(n < 0) continue;
        // 2.2 打印
        PrintPending(pending);
        sleep(1);
        cnt++;
        // 2.3 解除阻塞
        if(cnt == 4)
        {
            cout << "unblock 2 signo" << endl;
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }
    // 3. 发送2号信号 
    return 0;
}

运行结果:

1~31个信号中,只有9和19号信号不能被屏蔽。

四、信号的捕捉

1、信号什么时候被处理的?

当我们的进程从内核态返回到用户态的时候,进行信号的检测和处理。

内核态:允许我们访问操作系统的代码和数据。

用户态:只能访问用户自己的代码和数据。

2、谈谈地址空间

有几个进程,就有几份用户级页表 --- 进程具有独立性。

内核级页表只有一份。

每一个进程看到的3~4GB的东西都是一样的。整个系统中,不管进程如何切换,3~4GB空间的内容是不变的。

进程视角:我们调用系统中的方法,就是在我们自己的地址空间中进行执行的。

操作系统视角:任何一个时刻,都有进程在执行。我们想执行操作系统的代码,可以随时执行。

操作系统的本质:基于时钟中断的一个死循环。

计算机硬件中,有一个时钟芯片,每个很短的时间,向计算机发送时钟中断。

3、信号的捕捉流程

4、sigaction(注册信号处理函数)

act是输入型参数,oldact是输出型参数。

sa_handler:处理方法,sa_mask:用于屏蔽更多信号。

代码示例:

复制代码
#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>

1. pending位图,在执行信号捕捉方法之前从1->0,先清零,再调用
2. 信号被处理时,对应的信号也会被添加到block表中,防止信号捕捉被嵌套调用

 void PrintPending()
 {
     sigset_t set;
     sigpending(&set);

     for(int signo = 1; signo <= 31; signo++)
     {
         if(sigismember(&set, signo))
         cout << "1";
         else
         cout << "0";
     }
     cout << "\n";
 }

 void handler(int signo)
 {
     PrintPending();
     cout << "catch a signal, signal number: " << signo << endl;
     // while(true)
     // {
     //     PrintPending();
     //     sleep(1);
     // }
 }
 int main()
 {
     struct sigaction act, oact;
     memset(&act, 0, sizeof(act));
     memset(&oact, 0, sizeof(oact));

     // sigemptyset(&act.sa_mask);
     // sigaddset(&act.sa_mask, 1);
     // sigaddset(&act.sa_mask, 3);
     // sigaddset(&act.sa_mask, 4);
     act.sa_handler = handler;
     sigaction(2, &act, &oact);

     while(true)
     {
         cout << "I am a process: " << getpid() << endl;
         sleep(1);
     }
     return 0;
 }

5、可重入函数

insert函数被main和handler执行流重复进入,最后导致node2节点丢失,造成内存泄漏。

当一个函数被重复进入时,出错或可能出错,叫做不可重入函数,否则,叫做可重入函数。

大部分函数都是不可重入的。

6、volatile

关键字 volatile:防止编译器过度优化,保持内存的可见性。

代码示例:

复制代码
 volatile int flag = 0;

 void handler(int signo)
 {
     cout << "catch a signal: " << signo << endl;
     flag = 1;
 }

 int main()
 {
     signal(2, handler);

     while(!flag);
     //在优化条件下,flag变量可能直接被优化到CPU内的寄存器中
     cout << "process quit normal" << endl;

     return 0;
 }

mysignal:mysignal.cc
	g++ -o $@ $^ -O3 -g -std=c++11// -O3:最高级别的优化选项
.PHONY:clean
clean:
	rm -f mysignal

7、SIGCHLD信号

子进程退出的时候,会主动向父进程发送SIGCHLD(17)信号。

代码示例:

复制代码
#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

void handler(int signo)
{
    sleep(5);
    pid_t rid;
    while((rid = waitpid(-1, nullptr, WNOHANG)) > 0) // 非阻塞等待,若子进程不退出,返回0
    {
        cout << "I am process: " << getpid() << "catch a signo: " << signo <<
        "child process quit: " << rid << endl;
    }
}

int main()
{
    signal(17, SIG_IGN);//SIG_DFL -> action -> IGN
    // srand(time(nullptr));
    // signal(17, handler);

    for(int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            while(true)
            {
                cout << "I am process: " << getpid() << ", ppid: " << getppid() << endl;
                sleep(5);
                break;
            }
            cout << "child quit!" << endl;
            exit(0);
        }
        //sleep(rand()%5 + 3);
        sleep(1);
    }
    //father
    while(true)
    {
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

对于17号信号,SIG_IGN和SIG_DFL的区别:

1)signal(SIGCHLD, SIG_IGN)

父进程会忽略子进程结束的通知,内核会自动回收子进程的资源(避免产生僵尸进程)。

父进程不需要调用wait()或waitpid()来回收子进程。

2)signal(SIGCHLD, SIG_DFL)

系统默认行为是忽略信号,但内核不会自动回收子进程资源,子进程会变成僵尸进程。

父进程需要手动调用wait()或waitpid()来回收子进程的退出状态。

相关推荐
AlfredZhao18 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈2 天前
Unix 与 Linux 异同小叙
linux·服务器·unix
凡人叶枫2 天前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
2601_961875242 天前
决战申论100题2026|最新|范文
linux·容器·centos·debian·ssh·fabric·vagrant