0
点赞
收藏
分享

微信扫一扫

Vue + Springboot 文件上传项目笔记(一)

alonwang 2023-05-20 阅读 63

Linux-进程信号

一,信号入门

信号的概念

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

生活中的信号

  • 你在网上购买了许多商品,在等待快递的到来。即便快递没有到来,但是你也很清楚当快递来的时候,你要怎样处理快递。也就是说你能识别快递。
  • 当快递到来的时候,你收到快递到的信息,但是你正在做一件比较重要的事情,所以你并没有直接去取快递,但是你已经知道有快递来了。也就是说取快递的行为不一定要立即执行,可以理解为在“合适的时间”去取。
  • 在收到通知有快递要取,再到你拿到快递期间,是有一个时间窗口期的,在这段时间中,你并没有去取快递,但是你已经知道有一个快递已经来了。本质上是你记住了“有一个快递要去取”。
  • 到了合适的时间,你来取快递了。取到快递后就要对快递做处理。处理快递的情况有三种:1,执行默认动作(拆开快递立马使用商品)2,执行自定义动作(这个快递你是送给别人的礼物,所以你会把它送给别人)3,忽略快递(你对快递不感兴趣扔到一边继续做其他事情)。
  • 快递到来的这个过程对你来说是异步的,你不能准确断定快递具体什么时候到来。

技术应用角度的信号

#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
    while (true)
    {
        cout << "我是一个进程,我的pid : " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

🚀这段代码运行起来就会每隔一秒打印一句,用户可以通过Ctrl + C 来终止这个进程。Ctrl + C 的原理:首先用户在键盘上按下Ctrl + C按键,这个键盘会产生硬件中断,会被OS获取到,然后将输入的内容转化成信号,然后OS会把信号发送给目标进程。
前台进程收到此信号之后就会终止进程。
在这里插入图片描述
注意:
1.Ctrl + C产生的信号只能发送给前台进程。一个命令后加 &(./mytest &)就会放到后台运行,这样Shell就不必等待进程结束就可以接受新的命令,启动新的进程。
2.Shell可以同时运行一个前台进程和多个后台进程,只有前台进程才能接收到像Ctrl + C这种控制键产生的信号。
3.前台程序运行过程中,用户可能随时按下Ctrl + C而产生一个信号,也就是说进程的用户空间代码运行到任何地方都有可能接收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。

使用kill -l 查看信号列表

在这里插入图片描述
🚀1-31号信号是普通信号,其余的信号是实时信号,我们讨论的是普通信号。
🚀这些信号本质就是用#define 定义的宏。例如SIGINT 就是 #define SIGINT 2。
在这里插入图片描述
🚀可以通过man 7 signal 查看信号详细信息
在这里插入图片描述
🚀验证Ctrl + C控制键产生的是SIGINT信号,下面介绍以下signal这个接口。

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signal函数的作用就是可以用户自定义信号的处理方式,它的第一个参数是信号的编号(你可以使用2,当然也可以使用SIGINT),第二个参数是一个函数指针就是自定义处理该信号的方法,采用回调的方式完成的。

void handler(int signo)
{
    cout << "收到了 " << signo << " 号信号" << endl;
}
int main()
{
    signal(SIGINT, handler);
    while (true)
    {
        cout << "我是一个进程,我的pid : " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
事实证明了Ctrl + C控制键产生的信号就是2号SIGINT信号,默认执行动作是终止进程。

信号的处理方式

处理信号的方式一般有三种情况:
🚀执行该信号的默认执行动作。
🚀忽略此信号。
🚀自定义信号处理函数,当进程处理该信号时,由内核态切换到用户态时,就会执行该信号处理函数,这种方式叫做捕捉一个信号。

二,信号产生

通过终端按键产生信号

🚀上面实验的Ctrl+C控制键产生的是SIGINT信号,除此之外,Ctrl + \产生3号SIGQUIT信号。同样也可以试着对3号信号做捕捉。

void handler(int signo)
{
    cout << "收到了 " << signo << " 号信号" << endl;
}
int main()
{
    signal(SIGQUIT, handler);
    while (true)
    {
        cout << "我是一个进程,我的pid : " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
SIGINT信号默认动作是终止进程,SIGQUIT信号默认动作是终止进程 + Core Dump(核心转储)。

Core Dump

🚀对于云服务器来说是关闭核心转储功能的

ulimit -a #查看系统特定资源上限

在这里插入图片描述

ulimit -c #设置core file文件的大小

实验:
将核心转储功能打开后,让进程发生除0错误,那么此时OS就会给该进程发送SIGFPE信号(浮点数异常)让进程终止,由于SIGFPE信号的行为是终止进程并且发生核心转储,所以实验的现象应为,发生除0错误的进程被终止并且形成Core.pid文件。

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

int main()
{
    int a = 10 / 0;
    return 0;
}

在这里插入图片描述
🚀可以通过使用gdb并借助Core.pid文件进行事后调试

core-file core.pid  #打开core文件定位错误位置

在这里插入图片描述

code dump标志位

🚀子进程终止后会变成僵尸进程,父进程可以通过进程等待的方式来释放子进程的资源避免内存泄漏,同时还可以获取子进程的退出信息。获取子进程的退出信息是通过一个输出型参数status来完成的,status是int型变量,status的低7为表示进程收到的终止信号,第8位是core dump表示位,表示是否发生了核心转储,次低8位表示进程的退出码,所以通过父进程waitpid等待子进程,获取子进程的退出信息,可以知道子进程是否发生了核心转储。

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // child
        int a = 10 / 0;
        exit(1);
    }
    // parent
    int status = 0;
    waitpid(id, &status, 0);
    cout << "exit signal : " << (status & 0x7f) << " exit code : " << ((status >> 8) & 0xff)
         << " core dump : " << ((status >> 7) & 0x1) << endl;
    return 0;
}

在这里插入图片描述
注意:
core dump标志位表示的是是否发生了核心转储,意味着如果进程收到core类型的信号,但是核心转储功能是关闭的,core dump标志位也不会被置1。

通过系统调用向进程发信号

kill

int kill(pid_t pid, int sig);

在这里插入图片描述
🚀该系统调用的功能就是给指定进程发送指定信号,第一个参数为进程的pid,第二个参数为向进程发送的信号,成功返回0,失败返回-1。
🚀kill指令就是通过调用kill函数完成的。
🚀kill pid 默认向进程发送15号信号。

模拟实现一个mykill指令:

void Usage(char *str)
{
    cout << "Usage : " << str << " -pid -signo" << endl;
}
int main(int argc, char **argv)
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    string str1 = argv[1] + 1; //+1 是将-过滤掉
    string str2 = argv[2] + 1;
    kill(stoi(str1), stoi(str2));
    return 0;
}

通过mykill指令给一个死循环的进程发送SIGINT信号。
在这里插入图片描述

raise

int raise(int sig);

在这里插入图片描述
🚀只有一个参数为发送的信号,成功时返回0,失败是返回非0。
🚀与kill函数不同的是raise只能给当前进程发送信号,kill是可以给指定的进程发送信号。

abort

void abort(void);

🚀abort函数是让当前进程收到SIGABRT信号而异常终止。与exit函数一样,abort函数总是会成功的,所以没有返回值。

由软件条件产生信号

SIGPIPE

🚀在之前介绍使用管道来实现进程间通信时提到,如果管道的读端被关闭那么写端OS会杀死写端的进程,因为OS不会维护没有意义,低效率或者浪费资源的事情。OS就是向写端进程发送SIGPIPE信号,来杀死写端进程的。下面来验证以下:

void handler(int signo)
{
    cout << "我是父进程,我收到了" << signo << "号信号" << endl;
    exit(1);
}
int main()
{
    int pipe_fd[2];
    int n = pipe(pipe_fd);
    if (n == -1)
    {
        perror("pipe");
        exit(1);
    }
    pid_t id = fork();
    if (id == 0)
    {
        // child
        close(pipe_fd[1]);
        char buffer[1024];
        int cnt = 5;
        while (cnt--)
        {
            int n = read(pipe_fd[0], buffer, sizeof(buffer) - 1);
            buffer[n] = '\0';
            cout << "我是子进程,从管道中读取到的数据:" << buffer;
        }
        close(pipe_fd[0]);
        exit(2);
    }
    // parent
    close(pipe_fd[0]);
    signal(SIGPIPE, handler);
    while (true)
    {
        const char *str = "hello Linux\n";
        write(pipe_fd[1], str, strlen(str));
        sleep(1);
    }
    close(pipe_fd[1]);
    return 0;
}

在这里插入图片描述

SIGALRM

 #include <unistd.h>
 unsigned int alarm(unsigned int seconds);

🚀这个函数的返回值为0,或者是上次闹钟的剩余时间。就好比你中午准备睡一个小时的午觉,害怕睡过头所以你定了一个1小时的闹钟,但是刚过了30分钟就被人吵醒了,接着你又重新定了一个40分钟的闹钟,那么此时的返回值就是上次闹钟剩余的那三十分钟。如果给alarm的参数设置为0的话,表示取消掉之前的闹钟,返回值仍然是之前闹钟所剩余的时间。
可以写段代码来验证一下alarm的返回值。

在这里插入图片描述
🚀时钟的本质

🚀顺便通过alarm函数来验证一下与外设的IO是效率低下的。

int count = 0;
int main()
{
    alarm(1);
    while (true)
    {
        count++;
        cout << count << endl;
    }
    return 0;
}

在这里插入图片描述

int count = 0;
void handler(int signo)
{
    cout << "count : " << count << endl;
    exit(1);
}
int main()
{
    signal(SIGALRM, handler);
    alarm(1);
    while (true)
    {
        count++;
    }
    return 0;
}

在这里插入图片描述
🚀可以看到边计算边打印最终count被加到了三万多次,而一秒钟一直计算count被加到了五亿多次,可见与外设的IO是效率低下的。

硬件异常产生信号

🚀硬件异常发生后被某种硬件检测到后,通知给操作系统,然后操作系统会向产生异常的进程发送信号。常见的硬件异常产生的信号有野指针的访问产生的SIGSEGV信号,除0错误产生的SIGFPE信号。

🚀模拟野指针引起的异常

void handler(signo)
{
    cout << "收到了" << signo << "号信号" << endl;
}
int main()
{
    signal(SIGSEGV, handler);
    int *p = nullptr;
    *p = 100;
    return 0;
}

在这里插入图片描述
🚀为了解释为什么会一直死循环打印,先要了解野指针访问的是如何引起硬件异常的。

🚀除0操作产生的异常

三,信号阻塞

信号的相关概念

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

在内核中的表示

在这里插入图片描述
🚀在内核中存在三张与信号相关的表结构,分别是block表,pending表,和信号处理函数的函数指针数组。
🚀每个信号都有两个状态标志位,分别表示阻塞和未决,还有一个函数指针表示处理信号的动作。信号产生时,OS会在进程pcb中设置该信号的未决标志,直到信号递达后才删除该标志。
🚀对于上图中的1号信号,没有产生过,也没有被阻塞,当器被递达时会执行默认的处理动作。
🚀对于图中的2号信号,已经产生过其pending表中的位置被置1,并且也被阻塞block表中的位置被置1,其递达后的处理动作为忽略。但是没有接触阻塞之前并不能忽略此信号,因为进程有机会改变对这个信号的处理动作。
🚀对于图中的三号信号,没有产生过但是已经被阻塞,对信号的处理动作是用户自定义的。
🚀如果在对一个信号解除阻塞之前,收到了多次该信号,OS是这样处理的:首先系统允许给一个进程发送同一信号一次或多次。在Linux中,常规信号在递达之前产生多次那么系统只记录一次,而实时信号在递达之前产生多次,系统会都记录下来放在一个队列中。

sigset_t类型

🚀操作系统内定义了一种新的数据类型,这种数据类型就是一种位图结构用于我们获取或修改block表或者是获取pending表使用的。

在这里插入图片描述
以long int类型为4个字节的系统来说,这种数据类型定义了 32 * 1024 / (8 * 4) = 1024 个比特位,那么知道信号编号是如何与比特位对应的呢?起始也很简单以65为例,首先用65除以32得到属于第几个long int,再用65 % 32得到具体属于这个long int的第几个比特位。

信号集操作函数

🚀为了更方便的操作sigset_t类型,系统提供了一批函数。

  • int sigemptyset(sigset_t *set);
    将sigset_t类型的变量所有比特位清零。
  • int sigfillset(sigset_t *set);
    将所有比特位置1。
  • int sigaddset(sigset_t *set, int signum);
    在位图结构中将该信号对应的比特位置1。
  • int sigdelset(sigset_t *set, int signum);
    在位图结构中将该信号对应的比特位置0。
  • int sigismember(const sigset_t *set, int signum);
    判断信号集合中是否包含此信号。

🚀对于前四个函数成功返回0失败返回-1,最后一个函数如果信号在信号集合中返回1不在返回0,失败返回-1。

sigprocmask

  • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    第一个参数有三个选项:
    1,SIG_BLOCK:将set集合中信号添加到阻塞信号集中。
    2,SIG_UNBLOCK:将set集合中的信号在阻塞信号集中取消。
    3,SIG_SETMASK:将阻塞信号集设置为set集合中的信号。
    第二个参数是一个输入型参数,将传入的set设置阻塞信号集。
    第三个参数是一个输出型参数,将老的阻塞信号集传出。

🚀使用sigprocmask系统接口来屏蔽2号信号。

int main()
{
    // 在sigset_t类型中添加2号信号
    sigset_t set, oldset;
    sigemptyset(&set);
    sigemptyset(&oldset);
    sigaddset(&set, SIGINT);
    // 屏蔽2号信号
    sigprocmask(SIG_BLOCK, &set, &oldset);

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

在这里插入图片描述
🚀阻塞2号信号,然后在十秒后解除对2号信号的阻塞。(当信号之前被阻塞,当他解除阻塞的时候会被立即递达

int main()
{
    // 在sigset_t类型中添加2号信号
    sigset_t set, oldset;
    sigemptyset(&set);
    sigemptyset(&oldset);
    sigaddset(&set, SIGINT);
    // 屏蔽2号信号
    sigprocmask(SIG_BLOCK, &set, &oldset);
    int cnt = 0;
    while (true)
    {
        cout << "getpid : " << getpid() << endl;
        sleep(1);
        if (cnt++ == 10)
        {
            sigprocmask(SIG_SETMASK, &oldset, nullptr);
            // 因为oldset中没有阻塞任何信号,所以使用oldset设置阻塞信号集等于将阻塞信号集清空
        }
    }
    return 0;
}

在这里插入图片描述

sigpending

int sigpending(sigset_t *set);
🚀获取pending信号集,获取成功返回0,失败返回-1,set是一个输出型参数。

🚀将2号信号阻塞,不断的获取pending位图并打印,然后用户给该进程发送2号信号,可以观察到的现象就是pending位图2号位置由0变位1。

void ShowPending(const sigset_t &pending)
{
    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
int main()
{
    // 在sigset_t类型中添加2号信号
    sigset_t set, oldset;
    sigemptyset(&set);
    sigemptyset(&oldset);
    sigaddset(&set, SIGINT);
    // 屏蔽2号信号
    sigprocmask(SIG_BLOCK, &set, &oldset);
    // int cnt = 0;
    while (true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        sigpending(&pending);
        ShowPending(pending);
        //cout << "getpid : " << getpid() << endl;
        sleep(1);
        // if (cnt++ == 10)
        // {
        //     sigprocmask(SIG_SETMASK, &oldset, nullptr);
        //     // 因为oldset中没有阻塞任何信号,所以使用oldset设置阻塞信号集等于将阻塞信号集清空
        // }
    }
    return 0;
}

在这里插入图片描述

四,信号捕捉

内核态与用户态

🚀重新认识一下进程地址空间:

在这里插入图片描述

🚀用户态与内核态:

🚀操作系统的本质:

🚀进程是如何被调度的呢?

信号处理过程

🚀前面一直说信号会在合适的时候被处理,这个合适的时候是指进程从内核态切换到用户态的过程,进程从内核态切换到用户态之前会去检查block位图pending位图,如果某个信号的block位图为0pending位图为1,那么在此时就会处理该信号执行信号的处理动作,如果在handler表中对应的位置为SIG_DFL表示执行默认方法一般就是终止或暂停进程,执行完信号处理函数后恢复进程的上下文切换到用户态。如果handler表中对应的处理方法是SIG_IGN表示忽略该信号,那么进程直接切换回用户态。如果handler表中对应位置为用户自定义方法,那么由于用户自定义的处理方法是在用户态的,所以进程先从内核态切换到用户态执行用户自定义的处理方法,(执行完后不能直接返回用户态,因为此时进程的上下文保存在内核中),执行完后调用sigreturn系统接口返回内核态,最终再从内核态切换回用户态。

信号捕捉过程图解

在这里插入图片描述
🚀如果信号的处理动作是用户自定义时,那么信号捕捉的过程中会有两次检查处理信号的时间结点,第一次是由内核态切换回用户态前会检测信号,第二次是执行完用户自定义方法后经过sigreturn返回用户态,再经过内核态返回用户态的时候会第二次进行信号的检测。

sigaction

🚀之前提到可以通过使用signal函数来自定义信号处理方法,但是signal函数不如sigaction接口的功能丰富。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

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);//与实时信号有关不讨论
           };

验证pending位图是处理函数执行前被置0的

#include <iostream>
using namespace std;
#include <signal.h>
#include <unistd.h>
#include <cassert>
void ShowPending(sigset_t &set)
{
    int signo = 1;
    for (; signo <= 31; signo++)
    {
        if (sigismember(&set, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
void handler(int signo)
{
    cout << "正在处理 " << signo << " 号信号" << endl;
    int cnt = 10;
    while (cnt--)
    {
        sigset_t pending;
        int n = sigpending(&pending);
        assert(n == 0);
        (void)n;
        ShowPending(pending);
        sleep(1);
    }
    cout << "处理完毕" << endl;
}
int main()
{
    sigset_t set, oset;
    // 屏蔽2号信号--本质是将PCB中维护的三个表中的block位图结构中2号信号的位置置1。
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set, 2);
    sigprocmask(SIG_BLOCK, &set, &oset);

    // 捕捉2号信号
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, 3);
    struct sigaction sig;
    sig.sa_flags = 0;
    sig.sa_mask = mask;
    sig.sa_handler = handler;
    sigaction(2, &sig, nullptr);

    int cnt = 0;
    while (true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        int n = sigpending(&pending);
        assert(n == 0);
        (void)n;
        ShowPending(pending);
        if (cnt++ == 15)
        {
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

五,补充概念

volatile关键字

🚀volatile的作用是:保持内存的可见性,告知编译器,不要做被该关键字修饰变量的优化,对变量的任何操作都必须在真实的内存中进行操作。
案例:

int flag = 0;
void handler(int signo)
{
    cout << "flag from 0 to 1 " << endl;
    flag = 1;
}
int main()
{
    signal(SIGINT, handler);
    while (!flag)
        ;
    return 0;
}

对于这段代码,main执行流中首先对2号信号做了自定义捕捉,然后就是一个死循环。当进程收到2号信号的时候,信号处理函数会将flag从0修改为1,此时main执行流中的死循环终止,进程退出。
在这里插入图片描述
但是当编译器对代码做优化的时候,结果可能就不想我们想的那样了。gcc/g++ -O0 -O1 -O2等指定编译器的优化等级。

g++ -o mytest mytest.cc -std=c++11 -O2

在这里插入图片描述
可以看到虽然在信号处理函数中将flag修改为1,但是while循环却丝毫不受影响,编译器究竟做了什么手脚呢?通过汇编代码来一探究竟。

🚀优化后的汇编代码
在这里插入图片描述
🚀使用volatile关键字修饰flag后的汇编代码
在这里插入图片描述

SIGCHLD

🚀我们之前处理僵尸进程的方法就是父进程使用waitpid的方法等待子进程退出然后处理子进程的资源,获取子进程的退出信息,然而,父进程是如何得知子进程退出的呢?
事实上,子进程退出后,会给父进程发送SIGCHLD信号,但是父进程对于SIGCHLD的默认处理动作是忽略(父进程的handler表中对于SIGCHLD的位置填写的是SID_DFL-其默认动作是忽略),由此我们就可以得到一种新的处理僵尸进程的方法,就是自定义信号的捕捉方法,父进程收到子进程退出的信号再去waitpid清理资源,获取退出信息等。

pid_t id;
void handler(int signo)
{
    cout << "我是 pid :" << getpid() << " 收到来自 pid : " << id << "的" << signo << "号信号" << endl;

    while (1)
    {
        pid_t res = waitpid(-1, nullptr, WNOHANG);
        if (res > 0)
        {
            cout << "处理了" << res << "进程" << endl;
        }
        else
        {
            break;
        }
    }
}
int main()
{
    signal(SIGCHLD, handler);
    for (int i = 0; i < 8; i++)
    {
        id = fork();
        if (id == 0)
        {
            // child
            int cnt = 5;
            while (cnt--)
            {
                cout << "i am child , pid : " << getpid() << endl;
                sleep(1);
            }
            exit(1);
        }
    }

    while (true)
    {
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
🚀相比于上面的写法还有一种更简洁的写法但是只是用与Linux操作系统。

在这里插入图片描述

举报

相关推荐

0 条评论