个人技术分享

目录

一、进程与信号

1.进程与信号的关系

 2.信号的种类​编辑

3.信号的处理方式

 4.信号概念

二、 信号的产生

1.键盘组合键

2.kill命令

3.系统调用

1.kill

2.raise 

3.abort

4.异常

5.软件条件 

进程等待补充

三、信号的保存 

1.阻塞信号 

1.信号的相关概念

2.在linux内核中的表示 

3.sigset_t

4.信号集操作函数

1.sigset_t使用函数

2.sigprocmask(操纵block位图)

3.sigpending(查看pending位图)

四、信号的捕捉处理

1. 信号是如何被处理的?是什么时候处理的?

2.重谈进程地址空间

3.sigaction函数

4.可重入函数

5.volatile关键字

 五、SIGCHLD信号


一、进程与信号

1.进程与信号的关系

  1. 进程必须识别并且能够处理信号;即使信号没有产生,也要具备处理信号的能力;信号的处理能力,属于进程内置功能的一部分
  2. 进程即便没有收到信号,也能知道信号怎么处理
  3. 当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,要等到合适的时候
  4. 一个进程当信号产生,到信号开始被处理,就一定会有时间窗口,进程具有临时保存那些信号已经发生了的能力

ctrl+c为什么会杀掉我们的前台进程呢?

 linux中,每一次登录,一个终端一般会配上一个bash,每一个登录,只允许一个进程是前台进程,可以允许多个进程是后台进程。

前台进程和后台进程的一个区别是谁能拿到键盘输入

在运行一个进程时,我们只需要在后面加一个&符号,它就会变成后台进程,例如:

./myprocess &

如果要杀掉该进程,我们就需要查看该进程的pid,让后使用kill -9 pid,进程pid查看方法:

ps axj | grep myprocesss

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

 2.信号的种类

3.信号的处理方式

  1. 默认动作
  2. 忽略
  3. 自定义动作

验证:进程myprocess是收到了2号信号才终止的

1.signal函数,系统调用函数

        只需要设置一次,往后都有效

int signum:传哪个信号

sighandler_t handler:自定义的动作

signal是修改进程对特定信号处理的动作

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>

using namespace std;

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

int main()
{
    signal(2, myheader);//只需要设置一次,往后都有效
    while(true)
    {
        cout << "i am a process!" << endl;
        sleep(1);
    }
    return 0;
}

信号相对于进程的控制流程是异步的

键盘是如何向内核输入数据的呢?ctrl+c是如何变成信号的呢? ---  硬件

答:在电脑开机启动时,电脑会自动将操作系统加载到内存中,键盘输入数据,会向CPU发送一个硬件中断,键盘有自己独有的中断号,操作系统读取到CPU发送的中断号,在自己的中断向量表中查找该中断号对应的中断方法,对应的即是读取键盘数据的方法,随即操作系统通过该方法读取键盘数据,并存入对应缓冲区中,此过程重复至键盘不再发送硬件中断,拷贝完成后,操作系统会给CPU发送中断

如果键盘输入ctrl+c组合键,操作系统会判断数据,控制(ctrl+c转化为2号信号并发给前台进程)

 4.信号概念

        信号是进程之间事件异步通知的一种方式,属于软中断。信号模拟了中断

二、 信号的产生

1.键盘组合键

如

ctrl+c

发送2号信号 终止前台进程
ctrl+\ 发送3号信号 终止前台进程
ctrl+z 发送19号信号 暂停前台进程

不是所有的信号都是可以被signal捕捉的,比如19号信号,还有9号进程。

2.kill命令

3.系统调用

1.kill

int main(char argc, char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:\n\t, %s signnum pid\n", argv[0]);
    }
    int signnum = stoi(argv[1]);
    pid_t pid = argv[2];
    kill(pid, signnum);
    return 0;
}

2.raise 

 发送一个信号给调用者

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

这个raise相当于

kill(getpid(), 2);

3.abort

 

引起一个正常的进程终止,发送自己一个6号信号

这个函数,即使被signal捕捉,执行完被捕捉的函数,最后也会终止进程。

而kill -6 pid命令,在6号信号被捕捉后,并不会终止进程。

kill(getpid(), 6);

在6号信号被捕捉后,kill也不会终止进程

原因在于abort函数内部经过了特殊处理

无论信号的产生方式有多少种,最终都是操作系统统一发送给进程的,OS是进程的管理者

4.异常

        硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

我们的/0和野指针怎么怎么让操作系统知道的呢?

5.软件条件 

异常只会由硬件产生吗?

 alarm会发出14号信号。

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

using namespace std;

void work()
{
    cout << "Print log..." << endl;
}

void headler(int sigo)
{
    cout << "...get a sig, number: %d" << sigo << endl;
    int n = alarm(5);
    cout << "剩余时间: " << n << endl;
}

int main()
{
    signal(SIGALRM, headler);
    int n = alarm(50);
    while (true)
    {
        cout << "Proc is run..., pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

进程等待补充

status进程退出标识符

8-15位是表示退出码,0-7位是退出信号

而core dump(核心转储)标志位是一位数据0和1,表示是Term还是Core

int main()
{
    int id = fork();
    if(id == 0)
    {
        //child
        int cnt = 500;
        while(cnt--)
        {
            cout << "i am child process! pid: " << getpid() << " cnt: " << cnt << endl;
            sleep(1);
        }
    }
    //father
    int status = 0;
    int rid = waitpid(id, &status, 0); // 0代表阻塞等待
    if(rid == id)
    {
        cout << "child quit info, rid: " << rid << " exit code: " << ((status >> 8) & 0xFF) << " exit signal: " << (status & 0x7F) << " core dump: " << ((status >> 7) & 0x1) << endl;
    }
    return 0;
}

通过ulimit这个命令将core dump(核心转储)打开

1.使用Action为Term的信号

2.使用Action为core信号

在我们发出Action为core的信号

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

出现这个core.pid文件后,我们调试时,就可以定位到出错的是哪一行

gdb
core-file core.pid

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

而在现实实际中,尤其是大型服务器群,我们不要使用core dump功能。假设有人写出一个一启动就挂的服务,而在公司中,一般服务挂了之后需要自动再启动,就会导致一个问题。就是core.pid文件会把磁盘打满,将操作系统给挂掉,这是损失非常大的。所以再线上服务,就需要禁止core dump功能。

三、信号的保存 

什么是信号的发送呢?

信号是操作系统给进程的PCB发送,是结构体中一个int signal;32位的一个位图

  1. 比特位的内容是0 1,表示是否收到该信号
  2. 比特位的位置,表示信号的编号
  3. 所谓的"发信号"就是"写信号",本质就是操作系统修改进程PCB task_struct里位图signal对应的比特位

因为操作系统是进程的管理者,只有它能修改进程task_struct内部的属性!

为什么要信号保存呢?

进程收到信号之后,可能不会立即处理这个信号,信号不会被处理,就要有一个时间窗口。

1.阻塞信号 

1.信号的相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2.在linux内核中的表示 

每个信号都有两个标志位block(阻塞)、pending(未决),还有一个函数指针提供方法

block位图位为0表示阻塞,为1表示不阻塞

pending位图判定进程是否收到信号和收到了那些信号

函数指针类型

宏:

SIG_DFL:默认

SIG_IGN:忽略信号

SIG_ERR:

自定义函数

3.sigset_t

可以用来表示block或pending的位图数据结构

4.信号集操作函数

1.sigset_t使用函数
#include <signal.h>
int sigemptyset(sigset_t *set); //全置0
int sigfillset(sigset_t *set);  //全置1
int sigaddset (sigset_t *set, int signo); //插入
int sigdelset(sigset_t *set, int signo);  //删除
bool sigismember(const sigset_t *set, int signo);  //查找
2.sigprocmask(操纵block位图)

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1 

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。

如果set是非空指针,更改进程的信息屏蔽字,参数how指示如何更改。

如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset中,然后根据set和how参数更改信号屏蔽字。

假设当前的信号屏蔽字为mask

SIG_BLOCK

set包含了我们希望添加到当前信号屏蔽字的信号

相当于mask = mask | set

SIG_UNBLOCK

set包含了我们希望从当前信号屏蔽字解除阻塞的信号

相当于mask = mask & (~set)

SIG_SETMASK

设置当前信号屏蔽字为set所指向的值

相当于mask = se

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。 

注:9号和19号信号,不可被屏蔽

3.sigpending(查看pending位图)

sigset_t *set是一个输出型参数,可以将pending位图通过set带出来。 

void PrintPending(sigset_t& pending) 
{
    for (size_t i = 31; i >= 1; --i)
    {
        if(sigismember(&pending, i))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl
         << endl;
}

void headler(int signo)
{
    cout << "get a siganl: " << signo << endl;
    sleep(1);
}

int main()
{
    //0.对2号信号进行自定义动作
    signal(2, headler);
    //1.对2号信号进行屏蔽 -- 数据预备
    sigset_t bset, oset;   //这个位图bset是在用户栈上开辟空间创立的,属于用户
    sigemptyset(&oset);
    sigemptyset(&bset);
    sigaddset(&bset, 2); //我们将2号信号屏蔽了吗?没有,因为它还没有被设置到进程的task_struct里
    //1.2调用系统调用,将数据设置进内核
    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 == 20)
        {
            cout << "unblock 2 signal" << endl;
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }
    return 0;
}

四、信号的捕捉处理

1. 信号是如何被处理的?是什么时候处理的?

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

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

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

调用系统调用! -- 操作系统是自动会做"身份"切换的,用户身份变成内核身份或者反着来

int 80 -- 汇编指令,从用户态陷入内核态

过程:

  1. 先执行代码,出现了系统调用    ---  陷入内核态
  2. 执行操作系统代码。执行完毕,返回用户态前处理当前进程可以递送的代码    ---  返回用户态
  3. 自定义捕捉,执行我们的捕捉方法 ---  重新进入内核
  4. 最后在直接返回用户态,从上次中断的位置继续向下执行

4次状态切换

2.重谈进程地址空间

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

内核级页表有几份?        只有一份

每一个进程看到的3-4GB的内容都是一样的,整个系统中,无论进程再怎么切换,3-4GB内容都是不变的!

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

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

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

如何检测进程能否访问内核空间,检测CPU内部寄存器ecs,低两位为0能访问,为3不能访问

3.sigaction函数

函数原型

 #include <signal.h>

int sigaction(int signum, const struct sigaction *act, 
                                struct sigaction *oldact);

 struct sigaction {
               void     (*sa_handler)(int);  //函数指针
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;    //位图,额外要屏蔽信号
               int        sa_flags;
               void     (*sa_restorer)(void);
           };

示例:

void PrintPending() 
{
    sigset_t pending;
    sigpending(&pending);
    for (size_t i = 31; i >= 1; --i)
    {
        if(sigismember(&pending, i))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void headler(int signo)
{
    cout << " catch a signal: " << signo << endl;
    while(true)
    {
        PrintPending();
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));
    act.sa_handler = headler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 1);
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);

    sigaction(2, &act, &oact);
    while (true)
    {
        cout << " i am process" << endl;
        sleep(1);
    }
    return 0;
}

4.可重入函数

        如果一个函数,被重复进入的情况下,出错了,或者可能出错,不可重入函数!否则,叫做可重入函数。

        目前我们学习到的大部分函数都是不可重入的

5.volatile关键字

保持内存的可见性,防止编译器过度优化。

volatile int flag = 0;

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

int main()
{
    signal(2, headler);
    //在编译器优化下,flag有可能直接被优化进CPU内部的寄存器中。所以要在该变量前加关键字,volatile
    while(!flag);

    cout << "process quit normal" << endl;
    return 0;
}

 五、SIGCHLD信号

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

由此我们可以写出另一种进程等待方式,基于信号来进程等待

如果我们一次退出10个子进程呢?退出一半的子进程呢?

可以以非阻塞方式加while循环进行等待进程退出


void headler(int signo)
{
    sleep(1);
    pid_t rid;
    while(rid = waitpid(-1, nullptr, WNOHANG) > 0) //以非阻塞等待方式退出
        cout << "i am process: " << getpid() << " catch a signo: " << signo << " child process quit: " << rid << endl;
}

int main()
{
    signal(17, headler);
    for (size_t i = 0; i < 10; ++i)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //child
            while(true)
            {
                cout << "i am a child process, ppid: " << getppid() << " pid: " << getpid() << endl;
                sleep(1);
                break;
            }
            cout << "child quit " << endl;
            exit(0);
        }
    }
    //father
    while(true)
    {
        cout << "i am a father process! pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作 置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽 略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可用。

当我们不需要知道子进程退出结果,就可以直接采用signal(17, SIG_IGN);

我们默认对17号信号默认动作 -- SIG_DFL -- action -- IGN