Linux——进程间信号(超级详解!!)

news2025/4/6 2:34:11

索引

  • 一.初始信号
      • 1.什么是信号
      • 2.前后台进程
      • 3.信号的种类
      • 4.信号的管理
  • 二.信号产生前
      • 1.验证键盘是可以产生信号的
      • 2.通过系统调用接口发送信号
      • 3.由软件条件产生信号
      • 4.硬件异常产生信号
      • 5.总结
      • 6.core dump
  • 信号产生中
      • 1.信号在内核中的表示
      • 2.信号集操作函数
  • 信号产生后
      • 1.了解内核态和用户态
      • 2.内存如何实现信号的捕捉
      • 3.sigaction

一.初始信号

1.什么是信号

生活的角度: 红绿灯,闹钟,下课铃等
1.我们是如何得知这些东西的?有人教,(能够认识这些场景下的信号以及其表示的含义)也就是能够识别这些信号
2.我们提前知道这些信号产生时要做什么也就是我们已经提前知道了信号处理的方法

从上述可以看出

即使信号没有产生,我们已经具备了处理信号的能力!

因此:信号是给进程发送的,进程要具备处理信号的能力

1.该能力一定是预先已经早就有了的
2.进程能够识别对应的信号
3.进程能够处理对应的信号
这个能力是OS给我们提供的

对于进程来讲,即使信号还没有产生,我们进程已经具有识别和处理这个信号的能力了

2.前后台进程

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

当我们直接运行上述程序时,该程序会变成一个前台进程,此时直接ctrl+C可以直接终止,是因为ctrl+C可以发送一个信号给前台进程,使得该进程退出。
但当我们将前台进程变成后台进程时,其接不到类似ctrl + C的信号,也就无法退出了

理解用户按下Ctrl + C,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。

在这里插入图片描述
注意:

  • 1.只有前台进程才能收到Ctrl+C产生的信号,后台进程无法收到,一个运行进程的命令后面+&可以使得前台进程转化成后台进程,转化为后台进程之后shell不必等到进程结束才可以接受新的命令,可以直接启动新进程
  • 2.前台进程在运行过程中用户随时按下ctrl+C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能受到SIGINT(就是Ctrl+C)而终止,所以信号相对于进程的控制流程来说是异步的。

3.信号的种类

kill -l 可以显示信号列表

在这里插入图片描述
数字是信号编号,右侧是宏,二者一个意思

1-31是分时信号,产生信号了不用立即处理
34-64是实时信号,信号产生了就必须处理。
我们学习的是1-31的普通信号。

4.信号的管理

在这里插入图片描述
那么进程又是如何管理信号的呢?
是在进程的PCB中

eg:
task_struct {
uint32_t sig;//位图 0000 0000

}

位图的内容表示有没有该信号,位图的位置表示是哪一个信号,由于PCB是在内核数据结构,所以只有OS有资格修改位图,OS是进程的管理者,进程的所有属性的获取和设置只能又操作系统来设置,因此无论信号怎么产生,最终一定是OS帮我们进行信号的设置

下面我将从三个部分:信号产生前,信号产生中,信号产生后来叙述进程间信号

二.信号产生前

上述可以了解到,信号在OS中是由位图表示的,所以信号的产生OS发送给进程的时候不如说是写入信号。

1.验证键盘是可以产生信号的

sighandler_t signal(int signum, sighandler_t handler); 对信号设置回调

 #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

验证Ctrl + C是2号信号,我们先对
-定义捕捉动作当我们在键盘上按Ctrl+C的时候,如果调用了我们的自定义函数,验证成功!

void handler(int signo)
{
    cout << "i am a process ,我获取了一个信号: " << signo << endl;
}
int main()
{

    signal(SIGINT, handler);
    sleep(3);
    cout << "自定义信号捕捉函数设置完毕" << endl;
    while (true)
    {
        cout << "我是一个正在运行的进程" << endl;
        sleep(1);
    }
    return 0;
}

signal(SIGINT, handler); 这里不是在调用handler方法,只有信号产生的时候,才会调用handler方法.
实验结果如下
在这里插入图片描述
因此可以得出结论:Ctrl + C :本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理动作就是终止自己,然而现在我们设置了用户对信号的自定义处理动作。

Ctrl + C 产生2号信号
Ctrl +\ 产生3号信号,同样也是终止进程

注意:9号信号是不能设置自定义的,即使设置了,kill -9 PID 照样也可以杀死进程,因此9号信号也叫做管理员信号

2.通过系统调用接口发送信号

int kill(pid_t pid, int sig);不仅是一个命令,还是一个系统调用接口,表示对某个进程发送某个信号
我自己写一个mykill进程,该进程是调用了kill这个函数的,可以得出结论,代码如下

mykill.cc

static void Usage(const string &proc)
{
    cerr << " Usage :\n\t" << proc << " signo pid " << endl;
}
int main(int argc, char *argv[])
{

    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    if (kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
    {
        cerr << "kill :" << strerror(errno) << endl;
    }
 }

myproc.cc

while (true)
    {
        sleep(1);
        cout << "我的PID是: " << getpid() << endl;
    }

在这里插入图片描述
int raise(int sig); 给自身发信号
在这里插入图片描述

NAME
       abort - cause abnormal process termination

SYNOPSIS
       #include <stdlib.h>

       void abort(void);//直接终止进程

abort()直接终止进程
在这里插入图片描述

3.由软件条件产生信号

NAME
       alarm - set an alarm clock for delivery of a signal

SYNOPSIS
       #include <unistd.h>

       unsigned int alarm(unsigned int seconds);

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程。函数的返回值是0或者是设定闹钟时间的剩余秒数。

int cnt = 0;

void handler(int signo)
{
    cout << "i am a process ,我获取了一个信号: " << signo << endl;
    cout << "cnt: " << cnt << endl;
}
int main()
{
    signal(SIGALRM, handler);

    alarm(1);
    while (1)
    {
        cnt++;
    }
}

在这里插入图片描述
一秒钟之后就会捕捉SIGALRM信号

4.硬件异常产生信号

下面先列举两个崩溃的进程

在这里插入图片描述
猜想一下:上述两个进程崩溃的本质也是收到了某个信号,验证一下,先将每个信号都设置自定义动作,然后再运行。
在这里插入图片描述

进程崩溃的本质:是该进程收到了异常信号,因为硬件异常,而导致OS向目标进程发信号,进而导致进程终止的现象。
除0:CPU内部,状态寄存器,当我们除0的时候,CPU内的状态寄存器会被设置成为有报错:浮点数越界,CPU内部寄存器(硬件),OS就会识别到CPU内有报错->1,然后OS就会向目标进程发信号,目标进程在合适的时候处理信号,终止进程
越界&野指针 我们在语言层面使用的地址(指针),其实都是虚拟地址->物理地址->物理内存->读取对应的数据和代码,如果虚拟地址有问题,而地址转化的工作是由MMU(硬件)+页表(软件)构成,转化过程就会引起问题->表现在硬件MMU上->OS就会发现硬件出了问题->OS向目标进程发送信号->目标进程在合适的时候处理信号->终止进程

由此可得出结论:我们在C/C++ 中除0,内存越界等异常,在系统层面上是被当成信号处理的。

5.总结

  1. 上面所说的信号的产生,信号的发送,最终都是要由OS来执行的,因为OS是进程的管理者
  2. 信号不是被立即执行,而是在进程合适的时候
  3. 信号不是被立即执行的,那么信号就会被记录下来,记录在进程的PCB
  4. 一个进程在没有收到信号的时候,能否知道自己应该对合法信号做何处理?能,处理方式已经由之前的程序员写在内核了

6.core dump

Coredump叫做核心转储,是进程在运行时突然崩溃的一个内存快照。

pid_t wait(int *status);

       pid_t waitpid(pid_t pid, int *status, int options);

在这里插入图片描述
wait 与waitpid都有一个status参数,该参数是一个输出型参数,由OS填充,其他比特位在我之前的博客有提过,当生成Core dump文件的时候,该标记位会设置成1.

在这里插入图片描述
由上可以看出,并不是所有的信号都会生成core文件的,只有程序自身内部出了问题才会产生core文件。

int main()
{

    pid_t id = fork();
    if (id == 0) // 子进程
    {
        int *p = nullptr;
        *p = 10000; // 野指针问题
        exit(1);
    }
    int status = 0;
    waitpid(id, &status, 0);
    printf("exitcode: %d,signo: %d, core dump flag: %d \n", (status >> 8) & 0xff, status * 0x7f, (status >> 7) & 0x1);
}

运行结果
在这里插入图片描述
可以子进程的退出信号是11,符合野指针出错信号,但此时的core dump标记位还是0,为什么呢?
在这里插入图片描述
因为我是线上的的云服务器,厂商设置的默认core文件个数就是0,也就是说禁止生成core文件。
在这里插入图片描述
设置之后此时允许生成core文件。
在这里插入图片描述
但这不是真正的用途,此时我们要生成可调试版本的程序。
在这里插入图片描述

为什么线上的云服务器将core dump文件关闭?
因为云服务器上的产品一般都是release版本,但是我们生成的core文件是可调式版本的,并且如果线上的产品挂掉了,最重要的不是找bug而是重启,并且一旦服务挂掉了,会直接重启,eg一秒钟重启一万次的话,每次都有core文件的话,此时磁盘占据大量的文件,磁盘被打满了会危急到操作系统,很危险!
总结一下:

信号从产生到递达之间的状态称为信号未决
进程可以选择阻塞某个信号,当进程阻塞信号时,信号无法被抵达
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
阻塞和忽略是不同的,只要信号被阻塞那么信号即使产生了也不会被递达,忽略是信号处理的一个动作

信号产生中

1.信号在内核中的表示

信号在内核中的表示
在这里插入图片描述

信号在内核中task_struct指向三张表,三张表都是用
位图表示的。

pending:未决信号集,表示该信号是否产生
block:阻塞信号集,表示该信号是否被阻塞
handler:指向的是每个信号的自定义函数.
以上述的SIGQUIT为例,此时该信号未产生,一旦产生该信号,他的处理动作是用户的自定义函数sighandler,但是由于此时该信号被阻塞了,此时该信号不会抵达,除非接触对该信号的阻塞,才会抵达。

同时,如果一个信号最初未被阻塞,但是在某信号抵达之前,也可以说是该信号正在处理的时候,如果继续产生该信号,该信号也还是只会被记录一次,实时信号在抵达之前可以产生多次,这里不讨论。

2.信号集操作函数

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也一样,因此,未决和阻塞标志可以用相同的数据类型sigset_t来储存。
sigset_t类型对于每种信号用一个bit表示有效无效,这个类型内部如何储存这些bit则依赖于系统实现,我们不必关心,我们只要会使用如下几个函数就可以了。

typedef __sigset_t sigset_t;
......
typedef struct
  {
    unsigned long int __val[_SIGSET_NWORDS];
  } __sigset_t;

根据上述源码可以看到sigset_t的实现与系统自身有关,所以我们不必关心。
int sigemptyset(sigset_t *set)
初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号
int sigfillset(sigset_t *set)
使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号
int sigaddset(sigset_t *set, int signo)
该函数允许将一个指定的信号添加到一个自定义信号集中,也就是将该信号的标准为设为1,表示阻塞该信号。
int sigdelset(sigset_t *set,int signo)
与上述函数相反,表示解除该信号的阻塞
int sigismember(const sigset_t *set, int signo)
判断一个信号集的有效信号中是否包含某种信号,也就是检查是否屏蔽该信号,如果包含则返回1,反之0
int sigpending(sigset_t *set);
获取当前进程(谁调用,获取谁)的pending信号集,通过set参数传出,调用成功返回0,失败返回-1
int sigpromask(int how, const sigset_t *set, sigset_t *oset
成功返回0,失败返回-1
若oset非空:当前进程的信号屏蔽字通过oset传出
set非空:更改进程的信号屏蔽字
how:指示如何更改
如果oset 和 set都非空,则将原来的信号屏蔽字备份到oset,然后根据set和how参数更改信号屏蔽字。 假设当前的信号屏蔽字为mask,下述说明了how参数的可选值

SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号
SIG_UNBLOCKset包含了从当前信号屏蔽字接触的·信号
SIG_SETMASK设置当前信号屏蔽字为set所指向的值

V1.1版本的代码先指示将2号信号添加到信号屏蔽字中
预期结果:初始的pending信号集都是0,当我们向进程发送2号信号后,pending信号集中表示2号信号的比特位变成1

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

void showpending(sigset_t *pendings)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(pendings, sig))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
void handler(int signo)
{
    cout << "我是一个进程,刚刚获得了一个信号: " << signo << endl;
}
int main()
{
    // 2.屏蔽掉2号信号
    sigset_t bsig, obsig;
    sigemptyset(&bsig);
    sigemptyset(&obsig);
    // 2.1添加2号信号到信号屏蔽字中
    sigaddset(&bsig, 2);
    // 2.2 设置用户级的信号屏蔽字到内核中,让当前进程屏蔽2号信号
    sigprocmask(SIG_SETMASK, &bsig, &obsig);
    cout << "pid: " << getpid() << endl;
    signal(SIGINT, handler);
    // 1.不断获取当前进程的pending信号集
    sigset_t pendings;
    int cnt = 0;
    while (true)
    {
        // 1.1清空信号集
        sigemptyset(&pendings);
        // 1.2获取当前进程pending信号集(谁调用就获取谁)
        if (sigpending(&pendings) == 0)
        {
            // 1.3打印一下当前进程的pending信号集
            sleep(1);
            showpending(&pendings);
            cnt++;
        }

        cout << "cnt: " << cnt << endl;
    }

    return 0;
}

在这里插入图片描述
V2.0
先将所有的信号都屏蔽,在20秒之后解除2号和3号信号
下面只贴部分更改的代码

 sigset_t bsig, obsig;
    sigemptyset(&bsig);
    sigemptyset(&obsig);
    // 2.1添加1-31号信号到信号屏蔽字中
    for (int sig = 1; sig < 32; sig++)
    {
        sigaddset(&bsig, sig);
    }
    // 2.2 设置用户级的信号屏蔽字到内核中,让当前进程屏蔽2号信号
    sigprocmask(SIG_SETMASK, &bsig, &obsig);
    cout << "pid: " << getpid() << endl;
    signal(SIGINT, handler);
    signal(3, handler);
    // 1.不断获取当前进程的pending信号集
    sigset_t pendings;
    int cnt = 0;
    while (true)
    {
        // 1.1清空信号集
        sigemptyset(&pendings);
        // 1.2获取当前进程pending信号集(谁调用就获取谁)
        if (sigpending(&pendings) == 0)
        {
            // 1.3打印一下当前进程的pending信号集
            sleep(1);
            showpending(&pendings);
            cnt++;
        }

        if (cnt == 20)
        {
            // sigprocmask(SIG_SETMASK,&obsig,nullptr); 直接用该方法可以直接解除所有信号集
            sigset_t sigs;
            sigemptyset(&sigs);
            sigaddset(&sigs, 2);
            sigaddset(&sigs, 3);
            sigprocmask(SIG_UNBLOCK, &sigs, nullptr);
        }

        cout << "cnt: " << cnt << endl;
    }

在这里插入图片描述
根据上述结果可以看出,当我们将信号添加到信号集之后,我们向进程发送信号时,此时代表该信号的比特位由0 --> 1
解除信号屏蔽之后,就会重新由1 --> 0
但是根据结果可以看出 9号信号即使被屏蔽了还是可以杀死进程

信号产生后

1.了解内核态和用户态

上述提到信号产生后,OS系统是在什么时候处理信号呢?
实在合适的时候,那合适的时候具体是什么时候呢?
当前进程从内核态切换回用户态的时候进行信号的检测与处理!
每个进程都有自己的task_struct指向其虚拟地址,虚拟地址到物理地址的转化是通过页表实现的,而每个进程对于自己的用户空间3G是独立的,还有一份公共的内核页表,如下所示.
在这里插入图片描述
那么OS在不在内存中被加载?答案是肯定的
无论进程如何切换,我们都可以找到内核的代码和数据,前提是你要有足够的权利进行访问!
那么当前的进程如何具备权利访问内核页表乃至访问内核数据呢?
要进行身份切换。我们要让OS知道此时访问数据的是内核还是页表
CPU内有对应的状态寄存器CR3寄存器,当比特位是0的时候表示内核态,当比特位是3的时候表示用户态:

用户态:只能访问用户级页表
内核态:既能访问内核级页表也能访问用户级页表
内核态相比于用户态拥有更高的权限

那么一般什么时候会从用户态切换回内核态呢?

  • 系统调用的时候
  • 时间片到了进行进程间切换等

2.内存如何实现信号的捕捉

我们必须要了解一个知识:
我们的程序,会无数次直接或者间接的访问系统级软硬件资源(管理是OS),本质上,你并没有去操作这些软硬件资源,而是必须通过OS–>无数次陷入内核(1.切换身份 2.切换页表) -->调用内核的代码–>完成访问的动作–>结果返回给用户(1.切换身份 2.切换页表)–>得到结果
eg:
while(1);
仅仅是这一行代码存在从用户态切换成内核态吗?
一定是有的,因为每个进程都有自己的时间片,当时间片到了,需要转换成内核态然后更换内核级页表 -->为了保护上下文,执行调度算法–>切换新的进程–>恢复新进程的上下文–>再切换成用户态–>CPU执行的就是新进场的代码!

下面一个场景,当我们调用完系统调用之后,返回内核态时,检测出了错误;
在这里插入图片描述
快速记忆

在这里插入图片描述

3.sigaction

该函数可以读取和修改指定信号相关联的处理动作,成功返回0,失败返回-1.

#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);
};

参数解析
signum:指定信号编号
act:非空,根据指针act修改该信号的处理动作
oldact:非空,通过其传出该信号原来的处理动作
act和oldact都是sigaction结构体
上述有该结构体的具体组成
sa_handler:表示该信号的处理动作
当某个信号正在被处理时,内核会自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复成原来的信号屏蔽字,这样的话,当前进程正在被处理时,如果这个信号再次产生,该信号会被阻塞直到当前信号处理结束。
如果在调用信号处理函数时,除了希望自动屏蔽当前信号,还希望自动屏蔽其他信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时恢复成原来的状态
sa_flags设为0.

实验一:先不给sa_mask添加信号

void handler(int signo)
{
    cout << "我是一个进程,刚刚获得了一个信号: " << signo << endl;
    sigset_t pending;
    // 此时会永远在处理某个信号
    while (true)
    {
        sigpending(&pending);
        for (int i = 1; i <= 31; i++)
        {
            if (sigismember(&pending, i))
                cout << "1";
            else
                cout << "0";
        }
        cout << endl;
        sleep(1);
    }
}
int main()
{
    cout << "my pid: " << getpid() << endl;
    struct sigaction act, oldact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    // sigaddset(&act.sa_mask, 3);
    sigaction(2, &act, &oldact);
    while (true)
    {
        cout << "main running" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

实验二:给sa_mask添加信号

    sigaddset(&act.sa_mask, 3);

在这里插入图片描述

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

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

相关文章

Java Web3J :使用web3j监听、查询、订阅智能合约的事件

前面有文章写如何使用Docker-compose方式部署blockscout浏览器+charts图表,区块链浏览器已经部署成功了,同时我们在链上增加了治理投票流程,如何实时的把治理事件快速同步到浏览器呢?这时就想到了Web3J来监听智能合约的事件,来达到同步事件的效果 目录 Web3J简介功能简介m…

lv3 嵌入式开发-4 linux shell命令(进程管理、用户管理)

目录 1 进程处理相关命令 1.1 进程的概念 1.2 查看进程的命令 1.3 发送信号命令 2 用户管理相关命令 2.1 用户管理相关文件介绍 2.2 用户管理相关命令介绍 1 进程处理相关命令 1.1 进程的概念 进程的概念主要有两点&#xff1a; 进程是一个实体。每一个进程都有它自己…

【计算机组成原理】第一章 计算机概述

1.冯诺依曼结构&#xff1a; 计算机由运算器、控制器、存储器、输入设备、输出设备五大部件组成 运算器和控制器称为CPU&#xff1b;CPU和存储器称为计算机主机&#xff1b;其余输入、输出设备、外存储器称为计算机外部设备采用二进制表示数据和指令 指令由操作码&#xff08;…

20230831-完成登录框的按钮操作,并在登录成功后进行界面跳转

登录框的按钮操作&#xff0c;并在登录成功后进行界面跳转 app.cpp #include "app.h" #include <cstdio> #include <QDebug> #include <QLineEdit> #include <QLabel> #include <QPainter> #include <QString> #include <Q…

golang 通用的 grpc http 基础开发框架

go-moda golang 通用的 grpc http 基础开发框架仓库地址: https://github.com/webws/go-moda仓库一直在更新,欢迎大家吐槽和指点 特性 transport: 集成 http&#xff08;echo、gin&#xff09;和 grpc。tracing: openTelemetry 实现微务链路追踪pprof: 分析性能config: 通用…

CDN+GitHub搭建图床

前期搭建博客的时候&#xff0c;老是遇到图片无法加载、加载出错等等问题&#xff0c;很是烦恼。于是想搭建一个图床&#xff0c;进行个人博客图片的存储、显示使用。 ​ 利用GitHubjsDelivrPicGo搭建免费图床&#xff0c;CDN图床就是这么朴实无华&#xff0c;是基于免费CDN与免…

l8-d4 IP地址与端口号

一、分类IP 1.IP 地址及其表示方法 例&#xff1a; 2.IP 地址采用 2 级结构 3.分类的 IP 地址 任意一个IP地址我们都可以迅速的得出类别&#xff0c;并计算得出网络号 当一个主机通过两个网卡同时连接到两网络时&#xff0c;也就是该主机同时拥有两个IP地址&#xff0c;该主机…

【Java】网络通信基础

网络通信基础 IP地址概念格式特殊IP 端口号概念格式注意事项 认识协议概念作用知名协议的默认端口五元组协议分层OSI七层模型TCP/IP五层(或四层)模型网络设备所在分层网络分层对应封装和分用 IP地址 概念 IP地址主要用于标识网络主机、其他网络设备&#xff08;如路由器&…

简单入门--无约束线性模型预测控制

简单入门--无约束模型预测控制 一、模型预测控制是什么&#xff1f;二、无约束线性模型预测控制1. 表达式2. 最优解推导3.MATLAB代码 导读&#xff1a;下棋有高手和菜鸟&#xff0c;高手往往预测未来多步棋局发展&#xff0c;提前布局&#xff0c;而菜鸟只根据当前棋局做选择&a…

构建高效实时数据流水线:Flink、Kafka 和 CnosDB 的完美组合

当今的数据技术生态系统中&#xff0c;实时数据处理已经成为许多企业不可或缺的一部分。为了满足这种需求&#xff0c;Apache Flink、Apache Kafka和CnosDB等开源工具的结合应运而生&#xff0c;使得实时数据流的收集、处理和存储变得更加高效和可靠。本篇文章将介绍如何使用 F…

论文的开题报告怎么写?

最近收到很多私信&#xff0c;在问我关于开题报告的问题。基本都是毕业论文题目怎样选&#xff1f;系统好不好弄&#xff1f;开题报告怎么写啊&#xff1f;啥也不会怎样办呢&#xff1f;系统运行不会&#xff1f;查重问题呀&#xff0c;要马上交开题报告了等等。 毕业论文题目怎…

Python 之__name__的用法以及解释

文章目录 介绍代码 介绍 __name__ 是一个在 Python 中特殊的内置变量&#xff0c;用于确定一个 Python 文件是被直接运行还是被导入为模块。 文件作为模板导入&#xff0c;则其 __name__属性值被自动设置为模块名 文件作为程序直接运行&#xff0c;则__name__属性属性值被自动设…

【FPGA零基础学习之旅#12】三线制数码管驱动(74HC595)串行移位寄存器驱动

&#x1f389;欢迎来到FPGA专栏~三线制数码管驱动 ☆* o(≧▽≦)o *☆嗨~我是小夏与酒&#x1f379; ✨博客主页&#xff1a;小夏与酒的博客 &#x1f388;该系列文章专栏&#xff1a;FPGA学习之旅 文章作者技术和水平有限&#xff0c;如果文中出现错误&#xff0c;希望大家能指…

stm32之30.DMA

DMA&#xff08;硬件加速方法&#xff09;一般用于帮运比较大的数据&#xff08;如&#xff1a;摄像头数据图像传输&#xff09;&#xff0c;寄存器-》DMA-》RAM 或者 RAM-》DMA-》寄存器提高CPU的工作效率 源码-- #include "myhead.h" #include "adc.h"#…

小白的第一个RNN(情感分析模型)

平台&#xff1a;window10&#xff0c;python3.11.4&#xff0c;pycharm 框架&#xff1a;keras 编写日期&#xff1a;20230903 数据集&#xff1a;英语&#xff0c;自编&#xff0c;训练集和测试集分别有4个样本&#xff0c;标签有积极和消极两种 环境搭建 新建文件夹&am…

【Sentinel】Sentinel与gateway的限流算法

文章目录 1、Sentinel与Hystrix的区别2、限流算法3、限流算法对比4、Sentinel限流与Gateway限流 1、Sentinel与Hystrix的区别 线程隔离有两种方式实现&#xff1a; 线程池隔离&#xff08;Hystrix默认采用&#xff09;信号量隔离&#xff08;Sentinel默认采用&#xff09; 服…

2023.09.03 学习周报

文章目录 摘要文献链接题目亮点本文工作 题目亮点本文工作 题目亮点本文工作 大气污染物传输的相关内容总结 摘要 本周阅读了三篇论文&#xff0c;第一篇文章的核心为改进PageRank算法和标签传播算法实现大气污染物传输分析模型&#xff0c;第二篇文章的核心为将SOD、VGG和LST…

9.3.tensorRT高级(4)封装系列-自动驾驶案例项目self-driving-车道线检测

目录 前言1. 车道线检测总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习 tensorRT 高级-自动驾驶案例项目self-driving-车道…

AJAX学习笔记2发送Post请求

AJAX学习笔记1发送Get请求_biubiubiu0706的博客-CSDN博客 继续 AJAX发送POST请求 无参数 测试 改回来 测试 AJAX POST请求 请求体中提交参数 测试 后端打断点 如何用AJAX模拟form表单post请求提交数据呢&#xff1f; 设置请求头必须在open之后,send之前 请求头里的设置好比…

yolov5手机版移植

感谢阅读 运行export.py然后百度一个onnx转化工具下载yolov5移动版文件和ncnn修改代码CMakeLists.txt修改修改param的参数![在这里插入图片描述](https://img-blog.csdnimg.cn/7c929414761840db8a2556843abcb2b3.jpeg)yolov5ncnn_jni.cpp修改修改stride16和stride32完工 运行ex…