【Linux】信号--信号初识/信号的产生方式/信号的保存

文章目录

  • 一、信号初步理解
    • 1.生活角度的信号
    • 2.技术应用角度的信号
  • 二、信号的产生方式
    • 1.通过终端按键产生信号
    • 2.调用系统函数向进程发信号
    • 3.硬件异常产生信号
    • 4.由软件条件产生信号
    • 5.进程退出时的核心转储问题
  • 三、信号的保存
    • 1.信号其他相关常见概念
    • 2.信号在内核中的表示
    • 3.sigset_t
    • 4.信号集操作函数

一、信号初步理解

1.生活角度的信号

我们生活中有许许多多的信号,比如我们手机的电量低于20%的时候会提醒我们电量低,红绿灯,以及QQ消息提醒等。我们以红绿灯为例,我们是能够识别红绿灯的,是我们知道红绿灯是什么并且应该产生的对应行为,比如红灯停,绿灯行。那为什么我们能够识别红绿灯呢,这是因为有人教育过你,让你在大脑中记住了对应的红绿灯的属性或行为。当绿灯亮的时候,我们不一定要过马路,因为我们可能此时在跟朋友告别等其他更重要的事情,所以当信号灯到来的时候,我们不一定立马处理这个信号,信号可以随时产生(异步),我们可能做着更重要的事情。信号的到来和信号被处理的时间段,我们称为时间窗口,但是在此期间,我们必须要记住这个信号。对于绿灯亮的时候,我们可以过马路,也可以在路边跳了一段舞之后再过马路,也有可能你根本就不是在等绿灯,此时就会忽略绿灯的亮起,所以对于信号,我们有三种处理方式:默认动作,自定义动作和忽略动作

2.技术应用角度的信号

现在我们把上面的概念迁移到操作系统中。进程是如何识别信号的呢?认识+动作。进程本身是被程序员编写的属性和逻辑的集合即由程序员编码完成的。当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定会被立即处理,所以进程本身必须有对信号的保存能力。进程在处理信号的时候,一般有三种动作:默认,自定义,忽略

如果一个信号是发给进程的,而进程需要保存,那么应该保存在哪里呢,答案是task_struct中,那么如何保存呢,结构体中包含了信号的一个字段

struct task_struct
{
    ......
    unsigned int signal;
}

我们使用kill -l 命令可以查看所有的信号:

在这里插入图片描述

其中[1,31]为普通信号,[34,64]为实时信号

那么一个unsigned int如何保证31个信号呢。答案是采用位图的方式,用31个比特位表示31个信号,其中比特位的位置,代表信号编号,比特位的内容,代表是否收到该信号,0没有,1表示有

发生信号的本质是修改PCB中的信号位图。PCB是内核维护的数据结构对象,PCB的管理者是OS,那么就只有OS有权利修改PCB中的内容,所以无论是哪一种发生信号的方式,本质都是通过OS向目标进程发送信号,所以OS一定要提供发送信号处理信号的相关系统调用,所以我们之前使用的kill 命令,底层一定调用了对应的系统调用

二、信号的产生方式

1.通过终端按键产生信号

当我们的程序正在运行的时候,我们可以使用ctl + c的方式中断进程

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

using namespace std;

int main()
{
    // 1. 通过键盘发送信号
    while (true)
    {
        cout << "hello world" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

ctl + c是一个组合键,操作系统将ctl + c解释为2号信号–SIGINT。SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程

注意事项:

1.Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

2.Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。

3.前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

我们也可以使用kill指令来终止程序

#include <iostream>
#include <sys/types.h>
#include <unistd.h>

// 我写了一个将来会一直运行的程序,用来进行后续的测试
int main()
{
    while (true)
    {
        std::cout << "我是一个正在执行的进程,pid:" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

2.调用系统函数向进程发信号

1.kill函数可以向任意进程发送任意信号

#include <signal.h>
int kill(pid_t pid, int signo);
成功返回0,错误返回-1
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

static void Usage(const string &proc)
{
    cout << "\nUsage:"
         << proc << "pid signo\n"
         << endl;
}

// ./mysignal pid signo
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);

    int n = kill(pid, signo);
    if (n != 0)
    {
        perror("kill");
    }
    return 0;
}

在这里插入图片描述

这样我们就可以使用一个进程来向另外一个进程发送信号了

2.raise() 给自己 发送 任意信号 kill(getpid(), 任意信号)

#include <signal.h>
int raise(int signo);
成功返回0,错误返回-1
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

int main()
{
    // 2. 系统调用向目标进程发送信号
    int cnt = 0;
    while (cnt <= 10)
    {
        cout << "cnt:" << cnt++ << "pid" << getpid() << endl;
        sleep(1);
        if (cnt >= 5)
        {
            raise(9);
        }
    }
    
    return 0;
}

在这里插入图片描述

3.abort() 给自己 发送 指定的信号SIGABRT, kill(getpid(), SIGABRT)

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

int main()
{
    // 2. 系统调用向目标进程发送信号
    int cnt = 0;
    while (cnt <= 10)
    {
        cout << "cnt:" << cnt++ << "pid" << getpid() << endl;
        sleep(1);
        if (cnt >= 5)
        {
            abort();
        }
    }
    
    return 0;
}

在这里插入图片描述

注意事项:

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)

关于信号处理的行为的理解:有很多的情况,进程收到大部分的信号,默认处理动作都是终止进程
信号的意义:信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以一样!

3.硬件异常产生信号

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

我们知道,大多数的处理结果都是终止程序,但是我们也可以自动的控制接收到某种信号之后OS的处理行为

信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的.下面我们介绍signal 函数

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数功能:改变OS接收到信号之后的行为
参数
signum:信号的编号
handler:处理方法的函数指针
返回值:signal() 返回信号处理程序的先前值,如果出错则返回 SIG_ERR。在发生错误的情况下,会设置errno以指示错误的原因。

1.除0错误

我们知道,一旦程序出现了除0错误之后,程序就会直接崩溃,从而导致进程退出。那么,为什么除0错误会终止程序,答案是当前进程会收到来自操作系统的信号–SIGFPE

int main()
{
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int a = 10;
        a /= 0;
    }
}

在这里插入图片描述

现在我们对信号进行自定义捕捉

void catchSig(int signo)
{
    cout << "获取到一个信号,信号编号是: " << signo << endl;
    sleep(1);
}
int main()
{
	signal(SIGFPE, catchSig);
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int a = 10;
        a /= 0;
    }
}

在这里插入图片描述

OS如何得知应该给当前进程发送8号信号的-- OS怎么知道我除0了呢

这是因为在CPU中有一个状态寄存器,里面有一个溢出标记位,当我们进行除0的时候,数据发生了溢出,CPU发生了运算异常,此时溢出标记位就被置为了1,因为操作系统是软硬件资源的管理者,操作系统就发现了运算异常,就会给对应的进程发送8号信号

我们看下面的代码,我们把除0放在循环的外面:

void catchSig(int signo)
{
    cout << "获取到一个信号,信号编号是: " << signo << endl;
    sleep(1);
}
int main()
{
	signal(SIGFPE, catchSig);
	int a = 10;
    a /= 0;
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
    }
}

在这里插入图片描述

我们发现,我们明明只有一次除0了,为什么""获取到一个信号,信号编号是8"还是循环打印呢,这是因为,后面的代码还没有结束,程序继续执行,但是CPU中的溢出标记位还是1,所以就会继续接收到8号信号,所以就会一直打印。

受到信号,不一定会引起进程退出 – 没有退出,有可能还会被调度,CPU内部的寄存器只有一份,但是寄存器中的内容,属于当前进程的上下文!,你没有能力或者动作修正这个问题当进程被切换的时候,就有无数次状态寄存器被保存和回复的过程所以每一次恢复的时候,就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1

2.野指针

int main()
{
    signal(11, catchSig);
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int *p = nullptr;
        *p = 100;
    }
}

在这里插入图片描述

为什么 野指针 就会崩溃呢?因为OS会给当前进程发送指定的11号信号

MMU因为越界访问,发生了异常,告知操作系统之后,OS系统给指定的进程发生11号信号

4.由软件条件产生信号

1.管道

当我们把管道的读端关闭之后,OS系统就给写的进程发送SIGPIPE信号,写端就不再写了,这是由软件条件触发的

2.alarm函数 和SIGALRM信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程。

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

int main()
{
    alarm(1);
    while(true)
    {
        cout << "我在运行: " << getpid() <<endl;
    }
}

在这里插入图片描述

这里我们就可以只有alarm计算我们的计算机能够将数据累计多少次!

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

在这里插入图片描述

nt cnt = 0;

void catchSig(int signo)
{
    cout << "cnt: " << cnt << endl;
    exit(1);
}

int main()
{
    signal(SIGALRM, catchSig);

    alarm(1);
    while (true)
    {
        cnt++;
    }
    return 0;
}

在这里插入图片描述

我们从上面的对比可以看出,IO其实很慢

任何一个进程,都可以通过alarm系统调用在内核中设置闹钟,OS中可能会存在着很多闹钟,那么OS系统要不要管理这些闹钟呢,答案是要,管理方法是先描述,再组织

操作系统会为闹钟定义类似于一下的数据结构

struct alarm
{
    uint64_t when;//未来的超时时间
    int type;//闹钟的类型,一次性的还是周期性的
    task_struct *p;
    struct alarm* next;
};

这样我们就可以对闹钟的数据结构使用一个链表来链接起来,对闹钟的管理。就变成了对链表的管理,OS系统会周期性的检测这些闹钟,超时时就会发送SIGALARM给对应的进程,也可以使用更加高效的数据结构来进行管理,比如优先级队列。

总结:

上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者

信号的处理是否是立即处理的?在合适的时候

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

5.进程退出时的核心转储问题

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2

这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

在这里插入图片描述

2号和3号信号都是终止进程,但是他们的action一个是Term,一个是Core.

Term会直接终止进程,但是Core终止进程之后会用户空间内存数据全部 保存到磁盘上。在云服务器上,默认如果进程是core退出的,我们暂时看不大不存数据的现象,因为云服务器默认关闭了core file选项,如果需要查看,就需要打开云服务器的core file选项

我们可以使用ulimit - a选项进行查看

ulimit -a

在这里插入图片描述

我们可以使用 ulimit -c 1024进行设置

ulimit -c size
int main()
{
    while (true)
    {
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int *p = nullptr;
        *p = 100;
    }
}

在这里插入图片描述

core dumped就称为核心转储:当进程出现异常的时候,我们将进程对应的时刻,在内存中的有效数据转储到磁盘中

首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c1024

在这里插入图片描述

其中文件后缀的数字为进程的pid

保存的数据可以支持我们进行调试

在这里插入图片描述

三、信号的保存

1.信号其他相关常见概念

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

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

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

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

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

2.信号在内核中的表示

在这里插入图片描述

在这里插入图片描述

内核中为信号设置了pending位图和block位图,对于pending位图来说,比特位的位置,代表信号的编号,比特位的内容,表示是否收到了对应的信号,而block位图,比特位的位置表示信号的编号,比特位的内容表示是否阻塞了该信号,其次还维护了一个函数指针数组,数组的下标表示信号的编号,数组下标对应的内容,表示对应信号的处理方法。

需要注意的是,如果一个信号没有产生,并不妨碍它可以先被阻塞。

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

3.sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

4.信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1

sigprocmask

调用函数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,下表说明了how参数的可选值

在这里插入图片描述

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

sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1

下面我们写一个程序,先屏幕2号信号,然后发送2号信号,我们可以看到block位图中第二个比特位从0置为1的过程

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

#define BLOCK_SIGNO 2

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

int main()
{
    // 1.先尝试屏蔽指定信号
    sigset_t block, oblock, pending;

    // 1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    // 1.2 添加要屏蔽的信号
    sigaddset(&block, BLOCK_SIGNO);
    // 1.3 开始屏蔽
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2.遍历打印pending信号集
    int cnt = 10;
    while (true)
    {
        // 2.1初始化
        sigemptyset(&pending);
        // 2.2获取它
        sigpending(&pending);
        // 2.3打印它
        show_pending(pending);
        sleep(1);
    }
}

在这里插入图片描述

下面我们更改我们的代码,让10s之后,解除对2号信号的屏蔽

int main()
{
    // 1.先尝试屏蔽指定信号
    sigset_t block, oblock, pending;

    // 1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    // 1.2 添加要屏蔽的信号
    sigaddset(&block, BLOCK_SIGNO);
    // 1.3 开始屏蔽
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2.遍历打印pending信号集
    int cnt = 10;
    while (true)
    {
        // 2.1初始化
        sigemptyset(&pending);
        // 2.2获取它
        sigpending(&pending);
        // 2.3打印它
        show_pending(pending);
        sleep(1);

        if (cnt-- == 0)
        {
            sigprocmask(SIG_SETMASK, &oblock, &block);
            std::cout << "恢复对信号的屏蔽,不屏蔽任何信号" << std::endl;
        }
    }
}

在这里插入图片描述

我们发现,解除对2号屏蔽之后,我们最后的打印语句也没有执行,这是因为一旦对特定的信号进行解除屏蔽,一般OS要至少立马递达一个信号,此时2号信号递达,OS采取默认的行为,直接在内核态将进程退出,并没有返回到用户态,所以就没有打印

下面我们对代码进行更改,使得可以看到打印语句,并且可以看到比特位从1置为0的过程。

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

#define BLOCK_SIGNO 2

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

static void handler(int signo)
{
    std::cout << signo << " 号信号已经被递达" << std::endl;
}

int main()
{
    signal(BLOCK_SIGNO, handler);
    // 1.先尝试屏蔽指定信号
    sigset_t block, oblock, pending;

    // 1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    // 1.2 添加要屏蔽的信号
    sigaddset(&block, BLOCK_SIGNO);
    // 1.3 开始屏蔽
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2.遍历打印pending信号集
    int cnt = 10;
    while (true)
    {
        // 2.1初始化
        sigemptyset(&pending);
        // 2.2获取它
        sigpending(&pending);
        // 2.3打印它
        show_pending(pending);
        sleep(1);

        if (cnt-- == 0)
        {
            sigprocmask(SIG_SETMASK, &oblock, &block);
            std::cout << "恢复对信号的屏蔽,不屏蔽任何信号" << std::endl;
        }
    }
}

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue实现滑动验证

效果图&#xff1a; 源码地址&#xff1a;github文档地址&#xff1a; https://github.com/monoplasty/vue-monoplasty-slide-verify 使用步骤&#xff1a;1&#xff0c;安装插件&#xff1a; npm install --save vue-monoplasty-slide-verify 在main.js中使用一下&#xff…

HTML---初识CSS

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 一.CSS概念 CSS是层叠样式表&#xff08;Cascading Style Sheets&#xff09;的缩写。它是一种用于描述HTML文档外观样式的标记语言。通过CSS&#xff0c;开发者可以在不改变HTML标记结构的情况…

尚硅谷Docker笔记-高级篇

1.Docker复杂安装 1.1安装mysql主从复制搭建步骤 1.新建主服务器容器实例3307 docker run -p 3307:3306 --name mysql-master \ -v /mydata/mysql-master/log:/var/log/mysql \ -v /mydata/mysql-master/data:/var/lib/mysql \ -v /mydata/mysql-master/conf:/etc/mysql \ -…

运筹学经典问题(六):设施选址问题

问题描述 设施选址问题&#xff08;Facility Location Problem, FLP&#xff09;也成选址-分配问题&#xff0c;是企业面临的一类重要问题&#xff1a;在哪里建造设施&#xff1f;建造多少&#xff1f;以及将哪些客户分配给哪些设施去服务&#xff1f; 以物流业的航空站点选…

华为云sp2服务器系统根分区扩容后重启失败解决

lvm根分区扩容 概念&#xff1a; PV&#xff08;物理卷&#xff1a;Physical Volumes&#xff09; VG&#xff08;物理卷组&#xff1a;Volume Group&#xff09; LV&#xff08;逻辑卷&#xff1a;Logical Volumes&#xff09; R系 V10服务器&#xff1a; 显示当前Logic…

实验5:NAT配置

1.实验目的&#xff1a; 了解NAT的基本概念和功能 掌握NAT的配置方法和命令 观察和分析NAT的工作原理和流程 2.实验内容&#xff1a; 在路由器上配置静态NAT&#xff0c;实现内网主机通过公网IP地址访问外网服务器在路由器上配置动态NAT&#xff0c;实现内网主机通过公网I…

华为配置本地端口镜像示例(1:1)

图1 配置本地端口镜像组网图 组网需求 如图1所示&#xff0c;某公司行政部通过Switch与外部Internet通信&#xff0c;监控设备Server与Switch直连。 现在希望通过Server对行政部访问Internet的流量进行监控 配置思路 在Switch进行如下配置&#xff0c;实现Server对所有行政…

RHEL8_Linux下载ansible

本章内容主要介绍RHEL8中如何安装ansible ansible时如何工作的在RHEL8中安装ansible 1.ansible工作原理 如果管理的服务器很多&#xff0c;如几十台甚至几百台&#xff0c;那么就需要一个自动化管理工具了&#xff0c;ansible就是这样的一种自动化管理工具。 1&…

智能优化算法应用:基于黏菌算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于黏菌算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于黏菌算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.黏菌算法4.实验参数设定5.算法结果6.参考文献7.MA…

黑马点评03一人一单

实战篇-07.优惠券秒杀-实现一人一单功能_哔哩哔哩_bilibili 1.之前的问题 虽然解决了超卖问题&#xff0c;但是无法保证一人只能买一张&#xff0c;容易发生黄牛行为。 2.解决方案 2.1订单查询&#xff1a;判断该用户是否已下单 在库存判断之前&#xff0c;判断用户id和优惠…

Mac中nvm切换node版本失败

Mac中使用 nvm 管理 node 版本&#xff0c;在使用指令&#xff1a;nvm use XXX 切换版本之后。 关闭终端&#xff0c;再次打开&#xff0c;输入 node -v 还是得到之前的 node 版本。 原因&#xff1a; 在这里这个 default 中有个 node 的版本号&#xff0c;使用 nvm use 时&a…

想要在电脑桌面上使用手机便签怎么操作?

作为一名上班族&#xff0c;我们时常需要在电脑和手机之间同步使用便签&#xff0c;以记录工作、生活中的重要事项。然而&#xff0c;有些时候我们可能更习惯在手机上使用便签&#xff0c;但又希望在电脑桌面上也能够方便地查看和编辑这些便签。那么&#xff0c;如何在电脑桌面…

算法:存在重复元素 II (哈希表和滑动窗口)

哈希表 时间复杂度 O(n) 空间复杂度 O(n) /*** param {number[]} nums* param {number} k* return {boolean}*/ var containsNearbyDuplicate function (nums, k) {let map new Map()for (let [index, item] of nums.entries()) {if (!map.has(item)) {map.set(item, index)…

被带偏的中国云计算,重归正途

文 | 智能相对论 作者 | 叶远风 阿里云战略聚焦公共云&#xff0c;对整个云计算市场而言都是一场自我审视。 从市场背景、行业发展、中外对比等多个方面&#xff0c;业界舆论给出了大量详实的数据分析&#xff0c;已经对阿里云为什么要聚焦公共云有了结论&#xff0c;这里不…

【重点】23.合并K个升序链表

题目 法1&#xff1a;分治合并 class Solution {public ListNode mergeKLists(ListNode[] lists) {return merge(lists, 0, lists.length - 1);}public ListNode merge(ListNode[] lists, int l, int r) {if (l > r) {return null;}if (l r) {return lists[l];}int mid l…

网络协议介绍

一、网络层 1.网络层功能 ①定义了基于IP协议的逻辑地址 ②连接不同的媒介设备 ③在网络中选择最佳路径转发数据 2.使用版本 使用的IP协议版本 ipv4 ipv6 首部长度&#xff1a;IP头部的长度 3.标识符 确定数据的分片是否来自于同一个文件。 4.标志 代表数据过小&…

利用vue-okr-tree实现飞书OKR对齐视图

vue-okr-tree-demo 因开发需求需要做一个类似飞书OKR对齐视图的功能&#xff0c;参考了两位大神的代码&#xff1a; 开源组件vue-okr-tree作者博客地址&#xff1a;http://t.csdnimg.cn/5gNfd 对组件二次封装的作者博客地址&#xff1a;http://t.csdnimg.cn/Tjaf0 开源组件v…

使用Docker本地安装部署Draw.io绘图工具并实现远程访问协作办公

前言 提到流程图&#xff0c;大家第一时间可能会想到Visio&#xff0c;不可否认&#xff0c;VIsio确实是功能强大&#xff0c;但是软件为收费&#xff0c;并且因为其功能强大&#xff0c;导致安装需要很多的系统内存&#xff0c;并且是不可跨平台使用。所以&#xff0c;今天给…

论文润色降重哪个平台好 papergpt

大家好&#xff0c;今天来聊聊论文润色降重哪个平台好&#xff0c;希望能给大家提供一点参考。 以下是针对论文重复率高的情况&#xff0c;提供一些修改建议和技巧&#xff1a; 标题&#xff1a;论文润色降重哪个平台好――专业、高效、可靠的学术支持 一、引言 在学术研究中&…

jmeter,取“临时重定向的登录接口”响应头中的cookie

1、线程组--创建线程组&#xff1b; 2、线程组--添加--取样器--HTTP请求&#xff1b; 3、Http请求--添加--后置处理器--正则表达式提取器&#xff1b; 4、线程组--添加--监听器--查看结果树&#xff1b; 5、线程组--添加--取样器--调试取样器。 首先理解 自动重定向 与跟随…