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

news2024/11/26 13:50:03

文章目录

  • 一、信号初步理解
    • 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.coloradmin.cn/o/1311598.html

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

相关文章

《PCL多线程加速处理》-配准-icp

《PCL多线程加速处理》-配准-icp 一、效果展示二、具体实现三、代码一、效果展示 数据越大,速度提升效果越快 1、48万点 2、十万点 3、三万点 4、9000点 配准数据 二、具体实现

使用uniapp,引入uView遇到的问题(easycom无效解决方法)

在HBuilderX通过npm安装uView时&#xff0c;按照官网文档配置easycom无效 官网为&#xff1a; 4. 配置easycom组件模式 此配置需要在项目根目录的pages.json中进行。 温馨提示 uni-app为了调试性能的原因&#xff0c;修改easycom规则不会实时生效&#xff0c;配置完后&…

【前端】vscode 相关插件

一 插件&#xff1a; 01、ESLint 用来识别并检查ECMAScript/JavaScript 代码的工具 02、Prettier 用来格式化代码&#xff0c;如.js、.vue、css等都可以进行格式化 03、Vetur 用来识别并高亮vue语法 04、EditorConfig 用来设置vscode的编程行为 二、安装依赖 01、…

SpringSecurity 手机号登录

一、工作流程 1.向手机发送验证码&#xff0c;第三方短信发送平台&#xff0c;如阿里云短信。 2.手机获取验证码后&#xff0c;在表单中输入验证码。 3.使用自定义过滤器​SmsCodeValidateFilter​。 4.短信校验通过后&#xff0c;使用自定义手机认证过滤器​SmsCodeAuthentic…

利用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…

oracle aq java jms使用(数据类型为XMLTYPE)

记录一次冷门技术oracle aq的使用 版本 oracle 11g 创建用户 -- 创建用户 create user testaq identified by 123456; grant connect, resource to testaq;-- 创建aq所需要的权限 grant execute on dbms_aq to testaq; grant execute on dbms_aqadm to testaq; begindbms_a…

android studio 快捷输入模板提示

在Android开发中&#xff0c;我们经常会遇到一些重复性的代码&#xff0c;例如创建一个新的Activity、定义一个Getter方法等。为了提高开发效率&#xff0c;Android Studio提供了Live Templates功能&#xff0c;可以通过简化输入来快速生成这些重复性代码。 按下图提示设置&am…

GO并发编程综合应用

一.GO并发编程综合应用 1.生产者消费者模式 1.1需求分析 ​ 生产者每秒生产一个商品&#xff0c;并通过物流公司取货 ​ 物流公司将商品运输到商铺 ​ 消费者阻塞等待商铺到货&#xff0c;需要消费10次商品 1.2实现原理 1.3代码实现&#xff1a; package mainimport (&q…

Google DeepMind发布Imagen 2文字到图像生成模型;微软在 HuggingFace 上发布了 Phi-2 的模型

&#x1f989; AI新闻 &#x1f680; Google DeepMind发布Imagen 2文字到图像生成模型 摘要&#xff1a;谷歌的Imagen 2是一种先进的文本到图像技术&#xff0c;可以生成与用户提示紧密对齐的高质量、逼真的图像。它通过使用训练数据的自然分布来生成更逼真的图像&#xff0c…

服务器上配置jupyter,提示Invalid credentials如何解决

我是按照网上教程在服务器上安装的jupyter以及进行的密码配置&#xff0c;我利用 passwd()这个口令生成的转译密码是"argon...."。按照教程配置jupyter notebook配置文件里面的内容&#xff0c;登陆网页提示"Invalid credentials"。我谷歌得到的解答是&…

07用户行为日志数据采集

用户行为数据由Flume从Kafka直接同步到HDFS&#xff0c;由于离线数仓采用Hive的分区表按天统计&#xff0c;所以目标路径要包含一层日期。具体数据流向如下图所示。 按照规划&#xff0c;该Flume需将Kafka中topic_log的数据发往HDFS。并且对每天产生的用户行为日志进行区分&am…

cfa一级考生复习经验分享系列(三)

从总成绩可以看出&#xff0c;位于90%水平之上&#xff0c;且置信区间全体均高于90%线。 从各科目成绩可以看出&#xff0c;所有科目均位于90%线上或高于90%线&#xff0c;其中&#xff0c;另类与衍生、公司金额、经济学、权益投资、固定收益、财报分析表现较好&#xff0c;目测…

多架构容器镜像构建实战

最近在一个国产化项目中遇到了这样一个场景&#xff0c;在同一个 Kubernetes 集群中的节点是混合架构的&#xff0c;也就是说&#xff0c;其中某些节点的 CPU 架构是 x86 的&#xff0c;而另一些节点是 ARM 的。为了让我们的镜像在这样的环境下运行&#xff0c;一种最简单的做法…

双端队列和优先级队列

文章目录 前言dequedeque底层设计迭代器设计 priority仿函数数组中的第k个最大元素优先级队列模拟实现pushpop调整仿函数存储自定义类型 前言 今天要介绍比较特殊的结构&#xff0c;双端队列。 还有一个适配器&#xff0c;优先级队列。 deque 栈的默认容器用了一个deque的东西…

案例课7——百度智能客服

1.公司介绍 百度智能客服是百度智能云推出的将AI技术赋能企业客服业务的一揽子解决方案。该方案基于百度世界先进的语音技术、自然语言理解技术、知识图谱等构建完备的一体化产品方案&#xff0c;结合各行业头部客户丰富的运营经验&#xff0c;持续深耕机场服务、电力调度等场…

【普中】基于51单片机简易计算器显示设计( proteus仿真+程序+设计报告+实物演示+讲解视频)

目录标题 &#x1f4df;1. 主要功能&#xff1a;&#x1f4df;2. 讲解视频&#xff1a;&#x1f4df;3. 设计说明书(报告)&#x1f4df;4. 仿真&#x1f4df;5. 实物烧录和现象&#x1f4df;6. 程序代码&#x1f4df;7. 设计资料内容清单 【普中开发板】基于51单片机简易计算器…

日志框架Log4j、JUL、JCL、Slf4j、Logback、Log4j2

为什么程序需要记录日志 我们不可能实时的24小时对系统进行人工监控&#xff0c;那么如果程序出现异常错误时要如何排查呢&#xff1f;并且系统在运行时做了哪些事情我们又从何得知呢&#xff1f;这个时候日志这个概念就出现了&#xff0c;日志的出现对系统监控和异常分析起着…

Jenkins 添加节点报错

报错日志 Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.UnsupportedClassVersionError: hudson/remoting/Launcher has been compiled by a more recent version of the Java Runtime (cl…

react+datav+echarts实现可视化数据大屏

&#x1f4d3;最近有点闲&#xff0c;就学习了下react&#xff0c;没想到就把react学完了&#xff0c;觉得还不错&#xff0c;就打算出一把reactdatav的简易版可视化数据大屏供大家做个参考。 &#x1f4d3;效果如下 1下载必要的框架 &#x1f4d3; react路由 npm install re…

Rancher中使用promtail+loki+grafna收集k8s日志并展示

Rancher中使用promtail+loki+grafna收集k8s日志并展示 根据应用需求和日志数量级别选择对应的日志收集、过滤和展示方式,当日志量不太大,又想简单集中管理查看日志时,可使用promtail+loki+grafna的方式。本文找那个loki和grafana外置在了k8s集群之外。 1、添加Chart Repo …