【Linux】进程通信 | 信号

news2024/11/25 10:43:43

本篇博客让我们一起来康康信号部分的内容

系统为CentOS7.6,完整代码见 Gitee

文章目录

  • 1.什么是信号
    • 1.1 何为异步?
    • 1.2 信号的种类
    • 1.3 信号产生
    • 1.4 信号动作
  • 2.系统接口
    • 2.1 signal
      • 2.1.1 前台进程和后台进程
      • 2.1.2 循环捕捉所有信号
      • 2.1.3 信号9/19
    • 2.2 kill
      • 2.2.1 killall
    • 2.3 raise
    • 2.4 abort
    • 2.5 alarm
    • 2.6 sigset_t信号集
    • 2.7 sigprocmask
    • 2.8 sigpending
      • 2.8.1 屏蔽2号信号
      • 2.8.2 屏蔽所有信号
      • 2.8.3 解除屏蔽
    • 2.9 sigaction
      • 2.9.1 基本使用
      • 2.9.2 sa_mask
  • 3.软件崩溃的本质
    • 3.1 情景演示
    • 3.2 说明
  • 4.coredump
    • 4.1 开启该功能
    • 4.2 使用coredump
    • 4.3 为什么默认关闭?
  • 5.进程处理信号
    • 5.1 内核态/用户态
    • 5.2 信号检测
  • 6.可重入函数
  • 7.volatile
    • 7.1 示例
  • 8.子进程发送信号
  • 结语

1.什么是信号

在进程运行过程中,会出现各种各样的情况。操作系统需要用一套机制,来管理进程的事件

  • 进程退出
  • 进程停止
  • 进程恢复运行
  • ……

同时,这套管理机制是异步的,属于一种软(件)中断

和硬件中断打断处理器类似,软件中断打断进程的执,让其执行对应代码进行响应

1.1 何为异步?

以网购物品为例:当商品寄到自提点的时候,会给你发送一条取件的短信(信号)。此时我正在打游戏,没时间去处理这个快递(即取快递的行为并不是必须立马执行)

但这个时候,我已经知道有一个快递到了(知道自己获取到了一个信号)本质上就是知道了一会要去取快递(一会要处理信号)

当游戏一把打完了,我们就去取快递了(处理信号)

这就是一种异步的过程。因为你不知道你的快递什么时候会到站点,进程也不知道自己什么时候会收到一个信号

1.2 信号的种类

使用kill -l命令,我们可以看到目前linux系统下64种不同的类型。

其中前32为标准(Standard)信号,后32为实时(Real-time)信号;本篇博客只关注标准信号

这些信号,都是linux系统中预定义的

其中最常用的便是9号信号,来中断进程。平时我们最常用的CTRL+C,也是通过向进程发2号信号让进程退出的

[muxue@bt-7274:~/git]$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

include/linux/signal.h中,我们可以看到对信号的解释,以及其默认处理方法 default action

/*
 * In POSIX a signal is sent either to a specific thread (Linux task)
 * or to the process as a whole (Linux thread group).  How the signal
 * is sent determines whether it's to one thread or the whole group,
 * which determines which signal mask(s) are involved in blocking it
 * from being delivered until later.  When the signal is delivered,
 * either it's caught or ignored by a user handler or it has a default
 * effect that applies to the whole thread group (POSIX process).
 *
 * The possible effects an unblocked signal set to SIG_DFL can have are:
 *   ignore	- Nothing Happens
 *   terminate	- kill the process, i.e. all threads in the group,
 * 		  similar to exit_group.  The group leader (only) reports
 *		  WIFSIGNALED status to its parent.
 *   coredump	- write a core dump file describing all threads using
 *		  the same mm and then kill all those threads
 *   stop 	- stop all the threads in the group, i.e. TASK_STOPPED state
 *
 * SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
 * Other signals when not blocked and set to SIG_DFL behaves as follows.
 * The job control signals also have other special effects.
 *
 *	+--------------------+------------------+
 *	|  POSIX signal      |  default action  |
 *	+--------------------+------------------+
 *	|  SIGHUP            |  terminate	|
 *	|  SIGINT            |	terminate	|
 *	|  SIGQUIT           |	coredump 	|
 *	|  SIGILL            |	coredump 	|
 *	|  SIGTRAP           |	coredump 	|
 *	|  SIGABRT/SIGIOT    |	coredump 	|
 *	|  SIGBUS            |	coredump 	|
 *	|  SIGFPE            |	coredump 	|
 *	|  SIGKILL           |	terminate(+)	|
 *	|  SIGUSR1           |	terminate	|
 *	|  SIGSEGV           |	coredump 	|
 *	|  SIGUSR2           |	terminate	|
 *	|  SIGPIPE           |	terminate	|
 *	|  SIGALRM           |	terminate	|
 *	|  SIGTERM           |	terminate	|
 *	|  SIGCHLD           |	ignore   	|
 *	|  SIGCONT           |	ignore(*)	|
 *	|  SIGSTOP           |	stop(*)(+)  	|
 *	|  SIGTSTP           |	stop(*)  	|
 *	|  SIGTTIN           |	stop(*)  	|
 *	|  SIGTTOU           |	stop(*)  	|
 *	|  SIGURG            |	ignore   	|
 *	|  SIGXCPU           |	coredump 	|
 *	|  SIGXFSZ           |	coredump 	|
 *	|  SIGVTALRM         |	terminate	|
 *	|  SIGPROF           |	terminate	|
 *	|  SIGPOLL/SIGIO     |	terminate	|
 *	|  SIGSYS/SIGUNUSED  |	coredump 	|
 *	|  SIGSTKFLT         |	terminate	|
 *	|  SIGWINCH          |	ignore   	|
 *	|  SIGPWR            |	terminate	|
 *	|  SIGRTMIN-SIGRTMAX |	terminate       |
 *	+--------------------+------------------+
 *	|  non-POSIX signal  |  default action  |
 *	+--------------------+------------------+
 *	|  SIGEMT            |  coredump	|
 *	+--------------------+------------------+
 *
 * (+) For SIGKILL and SIGSTOP the action is "always", not just "default".
 * (*) Special job control effects:
 * When SIGCONT is sent, it resumes the process (all threads in the group)
 * from TASK_STOPPED state and also clears any pending/queued stop signals
 * (any of those marked with "stop(*)").  This happens regardless of blocking,
 * catching, or ignoring SIGCONT.  When any stop signal is sent, it clears
 * any pending/queued SIGCONT signals; this happens regardless of blocking,
 * catching, or ignored the stop signal, though (except for SIGSTOP) the
 * default action of stopping the process may happen later or never.
 */

这也意味着:即便没有接收到信号,进程也具备有识别和处理这个信号的能力!因为在系统中,已经给每一个进程和信号指定了默认动作!

1.3 信号产生

有很多情况会产生信号

  • 系统接口(kill命令)
  • 键盘产生(CTRL+R CTRL+\)
  • 软件条件(进程停止,进程运行完退出)
  • 硬件异常(比如除0错误)

1.4 信号动作

既然有默认动作,那肯定也有非默认的了。实际上,一个进程对信号的处理分为三种不同的方式

  • 默认动作
  • 自定义动作
  • 忽略

前面提到,一个进程并不一定需要立刻处理一个信号。那么它一定需要有一个办法来记住自己收到的信号。

而存储信号,是由进程的PCB来完成的!

细心的你可能会发现,进程中的信号一共是64个,刚好是8个字节!我们可以通过位图结构,用两个int类型来存放一个进程收到的各种信号。

image-20221118185053908

在系统内核中,分别有三个表,用来存放进程的信号。而这些信号在位图中的位置,就是在handler方法集中处理动作的下标

block - 1表示该进程屏蔽这个信号
pending - 表示进程收到了什么信号,1代表收到且未处理
handler - 每一个信号所对应的处理方法,默认/忽略/自定义

这一切都是处于进程PCB中的,只有操作系统能为我们管理。所以操作系统提供了相关的接口,方便我们对进程信号进行自定义设置。

pending表中的信号只能保存一个,如果一个信号尚未处理,该位图为1;另外一个相同信号到来的时候,会被直接丢弃掉。(pending表只能记住一个信号)

handler表中的两个宏如下:

  • SIG_DFL 默认方法
  • SIG_IGN 忽略
  • 忽略是信号处理的一种方式,我们能正常收到这个信号,处理方法是不管他

2.系统接口

2.1 signal

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

这个函数可以用于设置某个信号的处理方法。如果设置成功,则返回这个信号的旧处理动作

RETURN VALUE
   signal()  returns the previous value of the signal handler, or SIG_ERR on error. In the event of an error, errno is set to indicate the cause.

比如我们将键盘退出的2号自定义一个回调函数,那么就不能用ctrl+c终止这个进程

#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

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

int main()
{
    //将二号信号设置一个回调,其余信号不做处理
    signal(2, handler);
    cout << "进程信号已经设置完了" << endl;
    sleep(3);

    while (true)
    {
        cout << "进程正在运行: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

2.1.1 前台进程和后台进程

这里对ctrl+c的作用进一步描述,它只能用来中断一个前台进程

./test #我们直接运行一个进程,就是前台进程

之前这种直接运行进程,在bash上打印内容的方式,都是一个前台进程,可以用ctrl+c终止;我们可以在后面加上&设置为一个后台进程

&只是临时在后台运行,bash关闭后会终止;如果想持久在后台运行,需要在命令最前面加上nohup

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./test &
[1] 8898
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898
^C
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898
^C
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898
进程正在运行: 8898

这时候这个进程会一直在当前bash的后台打印,期间我们可以执行其他的命令,但是它依旧会不停的打印。ctrl+c无法终止这个进程,因为它并没有在前台运行!

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ps jax | grep test
22965  8898  8898 22965 pts/22   22965 S     1001   0:00 ./test

ps命令查看,可以看到其运行态为S;而前台进程,运行态为S+

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ps jax | grep test
22965  9664  9664 22965 pts/22    9664 S+    1001   0:00 ./test

不过,虽然我们不能用CTRL+C终止这个进程,但使用kill -2发送2号信号,是可以终止掉这个进程的(前提是没有自定义2号信号的方法)

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 进程信号已经设置完了
进程正在运行: 10464
进程正在运行: 10464
进程正在运行: 10464
进程正在运行: 10464
进程正在运行: 10464
[1]+  Interrupt               ./test
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 

2.1.2 循环捕捉所有信号

我们可以用一个for循环,捕捉所有的信号

//.....
//其余代码同上
int main()
{
    //对所有的进程信号都设置一个回调
    for (int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }
    //signal(2, handler);//将二号信号设置一个回调,其余信号不做处理
    cout << "进程信号已经设置完了" << endl;
    sleep(3);

    while (true)
    {
        cout << "进程正在运行: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

设置了之后,对应的信号都会调用我们自己写的函数。但有一个例外,那便是kill -9

image-20221118193453220

2.1.3 信号9/19

LINUX下,9号信号是一个管理员信号,具有杀死进程的最高权限,不能被自定义捕捉

你想啊,要是linux不对9号进行限制,那我把所有信号都捕捉了,岂不是这个进行没有办法被外部中止了?小病毒啊!😂

和9号信号一样不能被屏蔽的,还有19号信号SIGSTOP

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
process running: 7779
process running: 7779
process running: 7779
process running: 7779
process running: 7779

[1]+  Stopped                 ./tsig
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

2.2 kill

kill不仅是一个系统命令,同时还有一个系统接口;

一般这种情况,用man kill查看命令的文档,man 2 kill查看接口函数

之前我以为它只是一个用来干掉进程的命令(毕竟kill就是这个意思)现在才知道原来它的作用是给进程发信号

//kill - send signal to a process
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

它的返回值很简单,如果成功发送信号,则返回0,否则返回-1并且更新errno

RETURN VALUE
       On success (at least one signal was sent), zero is returned.  On error, -1 is returned, and errno is set appropriately

所以我们可以写一个简单的函数实现,来制作一个自己的kill命令

void mykill(int argc,char *argv[])
{
    if(argc != 3)
    {
        cout << "Usage: " << argv[0] << " signo-id process-id" <<endl;
        exit(1);
    }

    if(kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
    {
        cerr << "kill: " << strerror(errno) << endl;
        exit(2);//出现错误
    }
    exit(0);//正常执行
}
//argc和argv是命令行参数
//argc传入命令个数,包括./test
//argv传入命令的字符串地址
int main(int argc, char *argv[])
{
    mykill(argc,argv);

    return 0;
}

成功发送了信号!

image-20221118230540961

如果我们使用错误的时候,则会发送提示信息👍

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./mkill
Usage: ./mkill signo-id process-id

2.2.1 killall

这个接口可以通过进程名向所有这个名字的进程发信号

[muxue@bt-7274:~/git]$ killall tsig

通过测试可以发现,它发送的是第15号信号

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
process running: 5846
process running: 5846
process 5846 get signal: 15
process running: 5846
process running: 5846

2.3 raise

这个系统接口的作用是给自己发信号

#include <signal.h>
int raise(int sig);

返回0代表调用成功,非0代表失败

RETURN VALUE
       raise() returns 0 on success, and nonzero for failure.

用下面的代码进行测试,进程会不断的给自己发送2号信号

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

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

void TestSignal()
{
    signal(2, handler);//将二号信号设置一个回调,其余信号不做处理
    cout << "进程信号已经设置完了" << endl;
    sleep(3);
}

int main(int argc, char *argv[])
{
    TestSignal();//设置对进程信号的屏蔽
    while(1)
    {
        raise(2);
        sleep(1);
    }
    
    return 0;
}

此时能看到每一秒会调用我们自己写的handler方法,打印收到2号信号

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
process get signal: 2
process get signal: 2
process get signal: 2
process get signal: 2
process get signal: 2

2.4 abort

向自己发送6) SIGABRT信号

#include <stdlib.h>
void abort(void);

还是2.3中的代码,将raise(2)修改为abort(),同时捕捉6号信号。

此时能观察到我们自己写的handler方法的确被调用了,但是进程依旧终止了

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
process get signal: 6
Aborted
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$

这说明6号信号有一个特性:可以被捕捉执行自定义方法,但执行完毕之后需要退出

相比之下,9号信号是不能被捕捉


2.5 alarm

这个接口的作用是一个定时器,设定秒数,时间到了之后,会收到14) SIGALRM信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
RETURN VALUE
    alarm()  returns  the number of seconds remaining until any previously scheduled alarm was due to be delivered, or zero if there was no previously scheduled alarm.

用下面的代码进行测试

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
void handler(int signo)
{
    cout << "process get signal: " << signo << endl;
}

void TestSignal()
{
    //对所有的进程信号都设置一个回调
    for (int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }
    cout << "进程信号已经设置完了" << endl;
    sleep(3);
}

int main(int argc, char *argv[])
{
    TestSignal();//设置对进程信号的屏蔽
    alarm(4);//4s后向自己发送14信号
    cout << "set alarm, sleep" << endl;
    sleep(8);
    cout << "sleep finish"<<endl;
    
    return 0;
}

可以看到在休眠期间,进程收到了14号信号。此时进程并没有退出,而是继续休眠

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
set alarm, sleep
process get signal: 14
sleep finish
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 

如果我们不对14号信号自定义捕捉,则会直接退出进程

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
set alarm, sleep
Alarm clock
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 

另外需要注意的是,alarm信号本身不会让进程休眠。如果进程在alarm信号设定秒数之前结束,则什么事情都不会发生

2.6 sigset_t信号集

这是一个数据类型,其为block/pending位图的存储结构,被称作信号集/信号屏蔽字

虽然我们能直接使用这个类型, 但是对这个信号集中的位图操作必须要调用系统接口来完成

#include <signal.h>
int sigemptyset(sigset_t *set);//初始化位图(清空)
int sigfillset(sigset_t *set);//全部置为1
int sigaddset(sigset_t *set, int signum);//设置位图中某一位的数据
int sigdelset(sigset_t *set, int signum);//删除位图中某一位的数据
//判断某一位信号是否在该集合中
int sigismember(const sigset_t *set, int signum);

2.7 sigprocmask

更改或则获取当前进程的信号屏蔽字

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

其中第一个参数为处理方法,分别有下面三种

  • SIG_BLOCK 将set参数中包含的位数设置为屏蔽
  • SIG_UNBLOCK 将set参数中包含的位数解除屏蔽
  • SIG_SETMASK 将当前的信号屏蔽字设置为set(覆盖)

第三个参数是一个输出型参数。如果传入了oldset,那么旧的信号屏蔽字会被放入oldset

SIG_BLOCK
  The set of blocked signals is the union of the current set and the set argument.

SIG_UNBLOCK
  The signals in set are removed from the current set of blocked signals.  It is permissible to attempt  to  unblock  a  signal which is not blocked.

SIG_SETMASK
   The set of blocked signals is set to the argument set.
   
If oldset is non-NULL, the previous value of the signal mask is stored in oldset.

RETURN VALUE
       sigprocmask() returns 0 on success and -1 on error.  In the event of an error, errno is set to indicate the cause.

如果用该接口接触了对某一个信号的阻塞,那么在该函数return前,至少其中一个消息被送达

2.8 sigpending

获取当前进程的pending信号集

#include <signal.h>
int sigpending(sigset_t *set);

参数为一个输出型参数。正确获取返回0,否则-1

这时候我们就可以写一个简单的函数来打印当前进程的信号集了

//打印信号集的内容
void showPending(sigset_t* pdg_ptr)
{
    for(int i=1;i<32;i++)
    {
        if(sigismember(pdg_ptr,i))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

int main()
{
    sigset_t pdg;
    while(1)
    {
        sigemptyset(&pdg);//初始化信号集
        if(sigpending(&pdg)==0)//获取
        {
            showPending(pdg);//获取成功,打印
        }
        else
        {
            cout << getpid() << " get pending err"<<endl;
        }
        sleep(1);
    }
    return 0;
}

运行之后可以看到,程序一直在打印当前进程的信号集

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
start process: 30981
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000

不过此时我们并没有对信号进行屏蔽,所以给这个进程发信号会被立即处理(递达)不能在pending表中观察到现象

2.8.1 屏蔽2号信号

此时尝试使用sigprocmask来屏蔽某一个信号,再来观察情况

int main(int argc, char *argv[])
{
    //block掉2号信号
    sigset_t nsig,osig;
    sigemptyset(&nsig);
    sigemptyset(&osig);
    sigaddset(&nsig,2);//在nsig中设置2为1
    sigprocmask(SIG_BLOCK,&nsig,&osig);//添加屏蔽

    cout << "start process: " << getpid() << endl;
    sigset_t pdg;
    while(1)
    {
        sigemptyset(&pdg);//初始化信号集
        if(sigpending(&pdg)==0)//获取
        {
            showPending(&pdg);//获取成功,打印
        }
        else
        {
            cout << getpid() << " get pending err"<<endl;
        }
        sleep(1);
    }
    
    return 0;
}

可以看到当我们键入CTRL+C的时候,2号信号被block了没有处理,pending表上的2号信号就会变为1,且多次CTRL+C不会有变化

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
start process: 3608
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000

2.8.2 屏蔽所有信号

可以用一个循环设置所有的信号位,让当前进程屏蔽掉所有信号

int main(int argc, char *argv[])
{
    //block掉所有信号
    sigset_t nsig,osig;
    sigemptyset(&nsig);
    sigemptyset(&osig);
    for(int i=1;i<32;i++)
    {
        sigaddset(&nsig,i);//在nsig中设置2为1
    }
    sigprocmask(SIG_BLOCK,&nsig,&osig);//添加屏蔽

    cout << "start process: " << getpid() << endl;
    sigset_t pdg;
    while(1)
    {
        sigemptyset(&pdg);//初始化信号集
        if(sigpending(&pdg)==0)//获取
        {
            showPending(&pdg);//获取成功,打印
        }
        else
        {
            cout << getpid() << " get pending err"<<endl;
        }
        sleep(1);
    }
    
    return 0;
}

运行之后可以观察到,不管给这个进程发几号信号,都会被屏蔽显示在pending集中;9号信号依旧是老大哥,不受影响,依旧能干掉这个进程

image-20221120183259473

2.8.3 解除屏蔽

如果在设置屏蔽之后,休眠15s(在此期间接收信号)再接触对信号的屏蔽

int main(int argc, char *argv[])
{
    //block掉所有信号
    sigset_t nsig,osig;
    sigemptyset(&nsig);
    sigemptyset(&osig);
    for(int i=1;i<32;i++)
    {
        sigaddset(&nsig,i);//在nsig中设置2为1
    }
    sigprocmask(SIG_BLOCK,&nsig,&osig);//添加屏蔽

    TestSignal();//设置信号自定义处理
    cout << "start process: " << getpid() << endl;
    sigset_t pdg;
    int k=15;
    while(k--)
    {
        sigemptyset(&pdg);//初始化信号集
        if(sigpending(&pdg)==0)//获取
        {
            showPending(&pdg);//获取成功,打印
        }
        else
        {
            cout << getpid() << " get pending err"<<endl;
        }
        sleep(1);
    }

    //利用osig恢复之前的block表
    sigprocmask(SIG_SETMASK,&osig,nullptr);
    sigemptyset(&pdg);//初始化信号集
    if(sigpending(&pdg)==0)//获取
    {
        showPending(&pdg);//获取成功,打印
    }
    sleep(10);
    cout << "process quit"<<endl;

    return 0;
}

此时就能观察到,信号被立马处理,pending表变为全0

image-20221120184159874

2.9 sigaction

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
			struct sigaction *oldact);

这个函数的参数和signal函数很相似,不过都变为了一个结构体。这个接口也可以用来处理实时信号(不在文本考虑范围内)

  • 第一个参数是需要处理信号的编号
  • 第二个参数是自定义的action
  • 第三个参数是输出型参数,可以获取到旧的处理方法

设置成功后返回0,出错返回-1

这个结构体的成员如下

struct sigaction {
    void     (*sa_handler)(int);//对信号的处理方法
    void     (*sa_sigaction)(int, siginfo_t *, void *);//可忽略
    sigset_t   sa_mask;//参考2.8中的处理方法
    int        sa_flags;//设为0
    void     (*sa_restorer)(void);//可忽略
};

2.9.1 基本使用

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

//打印收到的信号
void handler(int signo)
{
    cout << "process " << getpid() << " get signal: " << signo << endl;
}

int main(int argc, char *argv[])
{
    struct sigaction nact,oact;
    nact.sa_flags = 0;
    nact.sa_handler = handler;
    sigemptyset(&nact.sa_mask);//初始化

    sigaction(2,&nact,&oact);

    while(1)
    {
        cout << "process running: " << getpid() << endl;
        sleep(2);
    }

    return 0;
}

运行之后,我们自定义捕捉了2号信号,成功调用自己的handler方法!

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
process running: 15029
process running: 15029
^Cprocess 15029 get signal: 2
process running: 15029
process running: 15029
^Cprocess 15029 get signal: 2
process running: 15029
process running: 15029
process running: 15029
^\Quit
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 

2.9.2 sa_mask

这个成员是一个sigset_t类型,用于当处理一个信号的时候,连带屏蔽其他信号;

  • 当一个进程正在处理A信号的时候,操作系统会把A信号自动添加入Block表,屏蔽该信号(不允许同时处理两个A信号,避免信号A的递归式处理

如果你想在处理2号信号的时候,阻塞掉3、4、5号信号,就可以对sa_mask进行设置,设置方法参考2.8的操作

因为现在我演示的自定义方法只是一个再简单不过的示例,实际上进程收到信号的时候需要根据不同情况进行不同的自定义处理,这些自定义处理的过程可能会很长。此时就可以block掉其他的信号,不让它们影响当前进程运行的自定义方法

3.软件崩溃的本质

之前我们经常会遇到软件出错奔溃的情况,那么奔溃的本质是什么呢?

3.1 情景演示

用下面的一个除零错误作为演示

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

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

void TestSignal()
{
    //对所有的进程信号都设置一个回调
    for (int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }
    cout << "进程信号已经设置完了" << endl;
    sleep(3);
}


int main(int argc, char *argv[])
{
    TestSignal();
    int a = 10;
    int b = 0;
    try
    {
        int c = a / b; // C++的除0不是异常,不会抛出
        //所以会直接linux系统运行报错
    }
    catch (const exception &e)
    {
        cerr << "a/0 err" << endl;
        abort();
    }
    catch (...)
    {
        cout << "base catch" << endl;
        abort();
    }
    return 0;
}

运行了之后,该进程会一直收到8号信号,直到我们手动kill掉这个进程

process 3947 get signal: 8
process 3947 get signal: 8
process 3947 get signal: 8
process 3947 get signal: 8
process 3947 get signal: 8
process 3947 get signal: 8Killed

你可能会觉得奇怪,不是用try/catch进行了异常处理吗?为什么没有用呢?

那是因为,在C++中,并不会将除零错误当作一个异常进行处理!

QQ图片20220413084241


我们自定义捕捉了8号信号,没能让进程终止。但此时这个进程已经出现了一个严重的bug,操作系统就会一直给进程发这个信号

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
Floating point exception
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 

相比之下,如果不自定义捕捉,则会直接报错+终止进程

8号信号是SIGFPE,FPE即为Floating point exception的缩写!

3.2 说明

崩溃的本质,是该进程收到了异常信号,从而终止。

以除零错误为例,CPU内部会有一个状态寄存器,检测到用户进行除零计算的时候,会将状态寄存器设置为浮点数错误。当操作系统检测到这个错误的时候,便会向当前正在运行的进程发送8号信号。而我们的进程在收到信号的时候,会处理这个信号,默认的处理方法就是终止进程!

同理,当我们访问一个野指针的时候,操作系统能在虚拟地址转换的时候发现这个问题,向我们的进程发送11号信号

int main(int argc, char *argv[])
{
    TestSignal();
    int *p;
    *p=20;
    return 0;
}

11) SIGSEGV 代表段错误,写OJ题目的时候这个报错很常见😂

process 6754 get signal: 11
process 6754 get signal: 11
process 6754 get signal: 11
process 6754 get signal: 11
process 6754 get signal: 11Killed

4.coredump

在进程控制的博客中,提到当进程因为信号终止的时候,其status中的0-7位会是对应的终止信号,而第8位是该进程的core dump标记位

image-20221120110639470

在1.2贴出来的源码注释中可以看到,有不少信号的默认动作是进行core dump,比如8号信号。那么这个东西到底是什么玩意呢?


通过fork创建子进程,让子进程除零产生8号信号,子进程退出

int main(int argc, char *argv[])
{
    int status;
    int id = fork();
    if(id == 0)
    {
        //子进程
        int b=0;
        int a = 10/b;
    }
    
    int ret = waitpid(id,&status,0);
    //打印子进程的退出信息
    printf("exitcode:%d signo:%d coredump: %d\n",(status>>8)&&0xff,status&0x7f,(status>>7)&0x1);

    return 0;
}

此时可以观察到,coredump标记位为0

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
exitcode:0 signo:8 coredump: 0

4.1 开启该功能

默认情况下,我们云服务器的core dump功能是被关闭的,需要我们手动开启;

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -a
core file size          (blocks, -c) 0  #coredump功能被关闭了
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 14691
max locked memory       (kbytes, -l) unlimited
max memory size         (kbytes, -m) unlimited
open files                      (-n) 100002
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 14691
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 

使用ulimit -a命令指定core file的大小

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -c 10000
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -a
core file size          (blocks, -c) 10000

再次运行刚刚的代码,可以看到标记位为1,并且产生了一个core.27908文件,这个文件的后缀是产生coredump文件的进程pid

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
exitcode:0 signo:8 coredump: 1
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ll
total 292
-rw------- 1 muxue muxue 593920 Nov 20 12:34 core.27908
-rw-rw-r-- 1 muxue muxue    194 Nov 20 10:31 makefile
-rw-rw-r-- 1 muxue muxue    601 Nov 20 09:57 mkill.cc
-rw-rw-r-- 1 muxue muxue    203 Nov 20 09:56 test.cc
-rwxrwxr-x 1 muxue muxue  13768 Nov 20 12:34 tsig
-rw-rw-r-- 1 muxue muxue   1772 Nov 20 12:34 tsignal.cpp
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 

vscode告诉我们这个不是一个普通的文本文件

image-20221120123842724

这个现象告诉我们,默认动作是coredump的信号,会让进程退出,将coredump标记位置为1并且产生一个core.文件

4.2 使用coredump

这个功能会将进程在运行中产生异常的上下文数据,执行core dump(核心转储)为一个文件,方便我们debug

如下所示,使用-g命令以debug模式编译test.cc,运行的时候可以看到除零错误之后跟了一个(core dumped)提示我们进行了core dump操作,对应产生了一个core.文件

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ g++ test.cc -g -o test
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./test
Floating point exception (core dumped)

这时候打开gdb,输入core-file 文件名加载文件,就可以直接定位到出错代码的位置!

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ gdb test
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/muxue/git/linux/code/22-11-16_signal/test...done.
(gdb) core-file core.31997
[New LWP 31997]

Core was generated by `./test'.
Program terminated with signal 8, Arithmetic exception.
#0  0x000000000040065c in main () at test.cc:13
13          int a=10/0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64
(gdb) 
(gdb) 

这可比我们手动debug找错误方便多了

QQ图片20220419102702

4.3 为什么默认关闭?

你可能会觉得,这个功能不挺好的吗,为啥默认没有开启呢?

先来看看这个文件的大小,足足有580KB

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ls -lht 
total 292K
-rw-rw-r-- 1 muxue muxue 1.8K Nov 20 12:37 tsignal.cpp
-rw------- 1 muxue muxue 580K Nov 20 12:34 core.27908

一般而言,服务端运行的一些进程,都需要保持稳定性。比如B站的服务器挂了,第一时间要做的是重启服务进程(并不是重启服务器机器)

如果设置了这个coredump,当服务器进程因为错误退出的时候,会生成一个core.文件;这时候有一个守护进程(用来监视并及时重启服务器进程)发现服务器进程退出了,就会重启它。

这时候又遇到了刚刚那个bug,服务器进程又退出了,守护进程又来重启它……

如此往复,就会生成非常非常多的core.文件,塞满我们的硬盘。

对于求稳为主的服务器而言,这可不是一个好事。所以云服务器上默认禁止了这个功能。

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -c 0
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ulimit -a
core file size          (blocks, -c) 0

umlimit -c 0指定大小为0关闭该功能


5.进程处理信号

前面八八了这么一大堆,进程到底是什么时候来处理信号的呢?

  • 开门见山:进程从内核态切换成用户态的时候,处理信号

5.1 内核态/用户态

在程序地址空间的博客中,提到了每一个进程都有1gb的内核空间;该内核空间用于内核级页表的映射,即映射操作系统的物理内存!

image-20221120185234159

有内核级页表的存在,无论进程怎么切换,都能找到操作系统内核的代码和数据,前提是有权限访问。

  • CPU中的CR3状态寄存器会标识当前进程处于内核态还是用户态
  • 内核态可以访问所有代码和数据,权限最高
  • 用户态只能访问当前进程自己的数据

当我们进程需要执行内核接口的时候,就需要将进程切换为内核态;运行完毕之后,切换回用户态。

当我们进程出现了异常,会从用户态切换成内核态,由操作系统检测相关异常并向进程发送对应信号。

当我们进程的时间片到了(需要切换进程)也会从用户态转为内核态,由操作系统来进行进程切换。

5.2 信号检测

当进程从内核态切换回用户态的时候,会进行信号的检测和处理。此时判断pending表中是否有未处理信号,以及该信号是否有被block。如果一个信号没有被block,则将该信号递达给进程,执行对应的处理方法

  • 执行用户的自定义方法时,应该以什么身份执行?

注意,当我们给一个信号指定了自定义处理方法,就代表该信号的处理方法是用户提供的。此时需要以用户的身份去执行这个代码,才能正确访问用户级页表。

这么做也能避免恶意代码的注入。如果有人在自定义方法中写一个修改系统内核的恶意代码,也能被操作系统发现并阻止。

这个过程可以用下面这张图来解释(并非完整过程,仅供参考理解)

image-20221121142458166

每次处理完信号后,会返回用户进程,从上一次中断的位置开始继续往后运行

6.可重入函数

//头插
void insert(Node* p)
{
	p->next=head;
	head=p;
}

上面这个函数是一个非常简单的链表头插函数

如果我们这个头插函数处理的是一个全局的链表,就可能会因为用户态、内核态的切换,函数重入造成错误

image-20221121211426452

所以insert就是一个不可重入函数!除了这个头插,还有一些其他的函数也符合这个特效:

  • 调用了malloc或者free(可能会多次malloc和多次free)
  • 调用了I/O库的函数

依此类推,如果一个函数只访问他自己的局部变量,不会影响其他参数。那么他就是一个可重入函数

7.volatile

之前的学习中就已经知道,这个关键字的作用是每一次访问变量的时候,都必须要去内存中取


假设我们进程中需要通过一个全局变量进行条件判断

int flag=0;
int main()
{
    if(flags)
    {
        //..
    }
    else
    {
        //..
    }
    return 0;
}

如果我们自定义捕捉了一个信号,收到该信号的时候,会修改flag,执行if/else语句中对应的代码。

由于编译器的优化问题,每一次访问flag的时候,它可能不会每次都去内存中取,就会出现一个问题

  • 寄存器中 flag=0
  • 经过自定义捕捉函数处理,内存中 flag=1

这两个flag在if条件中会导向不同的结果!

为了避免这种可能因为平台、编译器、优化问题导致的代码bug,我们需要告诉所有编译器,不准对flag变量做任何优化处理,必须要老老实实的去内存中拿这个变量的数据!

//volatile保持内存的可见性
volatile int flag = 0;

7.1 示例

gcc编译器可以通过-O2指定较高的优化等级

以下面的代码为例

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

// 保持内存的可见性
int flag = 0;

void handler(int signo)
{
    flag = 1;
    printf("\n更改flags: 0->1\n");
}

int main()
{   
    printf("process start %d\n",getpid());
    signal(2, handler);//自定义捕捉2号信号

    while (!flag)
        ;//啥事不干的循环
    
   	printf("process exit!\n");
    return 0;
}

运行之后,键入CTRL+C,你会发现进程依旧没有退出!理论上来说flags=1!flags为假,应终止循环,退出进程才对!

[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ gcc test.c -o test -O2
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ ./test
process start 22333
^C
更改flags: 0->1
^C
更改flags: 0->1
^C
更改flags: 0->1
^C
更改flags: 0->1
^\Quit
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ 

如果我们加上volatile关键字,则不会出现这个问题,进程能够正常退出

[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ gcc test.c -o test -O2
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ ./test
process start 23086
^C
更改flag: 0->1
process exit!
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ 

去掉gcc编译器的优化参数,去掉volatile关键字,会发现进程也能正常退出

[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ gcc test.c -o test
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ ./test
process start 23224
^C
更改flag: 0->1
process exit!
[muxue@bt-7274:~/git/linux/code/22-11-21_volatile]$ 

这就是编译器优化不同的影响!加上volatile关键字能避免这个问题,使代码运行能有唯一结果!

8.子进程发送信号

当子进程的状态变化的时候,会向父进程发送17号信号

void testfork()
{
    int status;
    int id = fork();
    if(id == 0)
    {
        //子进程
        cout << "chlid process: " <<getpid()<<endl;
        int b=0;
        int a = 10/b;
    }
    TestSignal();
    int ret = waitpid(id,&status,0);
    //打印子进程的退出信息
    printf("exitcode:%d signo:%d coredump: %d\n",(status>>8)&&0xff,status&0x7f,(status>>7)&0x1);
}

观察结果,可以看到父进程收到了子进程的17号信号,此时子进程因为错误退出

[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ ./tsig
进程信号已经设置完了
chlid process: 25319
process 25318 get signal: 17
exitcode:0 signo:8 coredump: 0
[muxue@bt-7274:~/git/linux/code/22-11-16_signal]$ 

除了退出时会发送信号,子进程暂停、继续运行的时候,都会向父进程发送信号

image-20221121215300234

结语

进程信号到这里就基本over了,干货满满!

如果对你有帮助,还请点个赞吧!!!

QQ图片20220416140203

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/52386.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人工智能数学基础--概率与统计13:连续随机变量的标准正态分布

一、引言 在《人工智能数学基础–概率与统计12&#xff1a;连续随机变量的概率密度函数以及正态分布》介绍了连续随机变量概率分布及概率密度函数的概念&#xff0c;并介绍了连续随机变量一个重要的概率密度函数&#xff1a;正态分布的概率密度函数的定义以及推导、使用场景&a…

培养出最多亿万富翁的美国大学TOP10榜单

若论世界大学排名&#xff0c;除了U.S. News、QS、软科、泰晤士这四大权威排名外&#xff0c;另有一些依据不同指标的排名。下面知识人网小编就推荐这份福布斯榜单给出的美国大学排名&#xff0c;供感兴趣的读者围观。 福布斯去年发布了一份全球亿万富豪榜&#xff08;World’s…

怎么看网站域名有没有收录 收录情况怎么样 网站收录查询

对于网站收录的概念&#xff0c;互联网中或者搜索引擎中已经有大量的相关定义。网站收录&#xff0c;指的是爬虫爬取了网页&#xff0c;并将页面内容数据放入搜索引擎数据库中这一结果。 怎么看网站域名有没有收录?录情况怎么样? 用站长工具查询网站收录的操作步骤&#xff1…

[TIST 2022]No Free Lunch Theorem for Security and Utility in Federated Learning

联邦学习中的安全性和实用性没有免费午餐定理 No Free Lunch Theorem for Security and Utility in Federated Learning 目录摘要简介2 相关文献2.1 隐私测量2.2 联邦学习2.2.1 FL 中的威胁模型。2.2.2 FL 中的保护机制。2.3 隐私-实用权衡3 一般设置和框架3.1 符号3.2 一般设置…

前端如何实现网页变灰功能的?

备注&#xff1a;本文大量摘取前端充电宝公众号相关文章&#xff0c;大家感兴趣可以关注该公众号进行阅读学习 目录 1.引入 2.页面灰色实现方法 3.filter其他属性 A.blur()&#xff1a;模糊 B.brightness()&#xff1a;亮度 C.contrast()&#xff1a;对比度 C.opacity()…

unity---Mesh网格编程(六)

目录 1.模型切割 2.代码 1.模型切割 如图&#xff0c;对3D模型的Mesh网格进行切割&#xff0c;会经过若干个三角面。而切割后&#xff0c;将会产生新的面来组成左右两边的物体。 要记录每个顶点与顶点下标&#xff0c;新的面要顺时针绘制&#xff0c; 2.代码 using System.…

docker+nginx 安装部署修改资源目录配置文件和容器端口信息

查看docker镜像 可以先查看docker下是否存在nginx镜像&#xff0c;使用如下这些命令查看&#xff1a; docker images: 列出所有镜像。docker images nginx: 列出所有nginx镜像&#xff0c;不同版本等等。docker search nginx: 搜索查看所有nginx镜像信息。 拉取安装nginx镜像…

Java8 函数式编程【基础篇】

Java 8是Java在保持向后兼容的前提下首次迈出重要一步&#xff0c;相比之前&#xff0c;不再是只对类库的改良&#xff0c;在编写复杂的集合处理、并行化执行、代码简洁度等方面都有颠覆性的提升。本文将探索和理解函数式编程的含义&#xff0c;以及它在Java 8中的实现。 一、…

RESTful API是什么?看完你整个人都通透了

要弄清楚什么是RESTful API&#xff0c;首先要弄清楚什么是REST&#xff1f; 01 REST REpresentational State Transfer&#xff0c;英语的直译就是“表现层状态转移”。如果看这个概念&#xff0c;估计没几个人能明白是什么意思。那下面就让我来用一句话解释一下什么是RESTf…

低代码搭建质量管理解决方案,为企业管理提速降本

市场竞争的越来越卷&#xff0c;越来越多的制造企业认识到质量管理的重要性。尤其随着全球经济化与信息化的到来&#xff0c;质量管理已经成为企业管理的关键环节。然而与国际上具有先进技术和管理水平的企业相比&#xff0c;我国企业的质量管理较为薄弱&#xff0c;存在着质量…

【MATLAB】羽状图

目录 羽状图 羽状图 h0figure(toolbar,none,...position,[200 150 450 350],...name,实例28);subplot(2,1,1)alpha90:-10:0;rones(size(alpha));malpha*pi/180;nr*10;[u,v]pol2cart(m,n);feather(u,v)title(羽状图)axis([0 20 0 10])subplot(2,1,2)t0:0.5:10;x0.05i;yexp(-x*t…

R语言解释生存分析中危险率和风险率的变化

危险率函数 让我们模拟R中的一些数据&#xff1a; n < - 10000 h < - 0.5 t < - -log&#xff08;runif&#xff08;n&#xff09;&#xff09;/ h 该代码模拟了危险函数的存活时间&#xff0c;即常数。 视频&#xff1a;R语言生存分析原理与晚期肺癌患者分析案…

Ajax学习:jQuery发送ajax请求 通用方法$.ajax

app.all(/jQuery,(requset,response)>{response.setHeader(Access-Control-Allow-Origin,*);const data{name:张三};let strJSON.stringify(data);//需要转换称为json 否则传递的任然是对象response.send(str);//3s之后返回给客户端 }) $(button).eq(2).click(function() {/…

MySQL和Oracle JDBC驱动包下载步骤

MySQL官网&#xff1a;https://www.mysql.com/ 步骤如下&#xff1a; 1.点击DOWNLOADS 2.往下滑&#xff0c;找到MySQL Community&#xff08;GPL&#xff09;Downloands并点击 3.点击Connector/J 4.当前页面展示的是最新版本&#xff0c;要下载历史版本点击Archives 5.选择…

15 【登录鉴权】

15 【登录鉴权-Cookie】 1.什么是认证&#xff08;Authentication&#xff09; 通俗地讲就是验证当前用户的身份&#xff0c;证明“你是你自己”&#xff08;比如&#xff1a;你每天上下班打卡&#xff0c;都需要通过指纹打卡&#xff0c;当你的指纹和系统里录入的指纹相匹配…

细数APDL中的流程控制命令

作者&#xff1a;水哥ANSYS&#xff0c;获授权转载 一、概述 有过其他编程语言经验的同学都知道&#xff0c;流程控制类语言命令在编程中是必须掌握的一门技巧&#xff0c;这类命令能大幅提高我们的编程效率&#xff0c;增加程序可读性。类似地&#xff0c;在APDL中也有很多的…

R语言Poisson回归的拟合优度检验

在这篇文章中&#xff0c;我们将看一下Poisson回归的拟合优度测试与个体计数数据。 最近我们被客户要求撰写关于Poisson回归的研究报告&#xff0c;包括一些图形和统计输出。许多软件包在拟合Poisson回归模型时在输出中提供此测试&#xff0c;或者在拟合此类模型&#xff08;例…

不刷题,PMP考试可以通过吗?

不能&#xff0c;除非你的项目管理经验很厉害&#xff0c;但这么厉害也不需要PMP这个证书了&#xff0c;做一个比喻&#xff0c;学习项目管理知识是读兵书&#xff0c;做题就是“纸上谈兵”&#xff0c;获得PMP证书就是证明你有做指挥的资格&#xff0c;做项目是上战场指挥打仗…

提交代码出现error Empty block statement no-empty,代码却没报错?

开开心心写完代码&#xff0c;commit一下&#xff0c;发现可能控制台报错了&#xff1a; 看了代码却没发现有报错的&#xff0c;后来发现是开了eslint校验&#xff01; 因为存在空的if体&#xff0c;如下&#xff1a; 解决&#xff1a;保证if体不为空即可 参考&#xff1a;…

excel转换成pdf格式怎么操作?这3招教你Excel怎么转PDF

在我们日常办公中&#xff0c;经常会需要用到Excel表格&#xff0c;这类文件格式可以帮助我们日常记录统计数据&#xff0c;有效的提升办公效率。当我们需要将文件发送给别人&#xff0c;为了避免被改数据内容&#xff0c;很多时候都会将Excel转换为PDF格式。那么&#xff0c;E…