[ Linux ] 进程信号递达,阻塞,捕捉

news2024/11/15 20:26:08

目录

1.core dump字段

1.1 Core dump是什么?

1.2 用代码看看Core Dump

1.3 core dump的作用

core dump一般会被关掉

2.阻塞信号

2.1 信号其他相关常见概念

2.2在内核中的表示

3.信号产生中

3.1 sigset_t

3.2信号集操作函数

3.2.1 sigprocmask

3.2.2 sigpending

3.3 使用程序查看pending表

3.3.1手动设置pending表

3.3.2 恢复信号

4.信号捕捉

4.1用户级页表和内核级页表

4.2进程的信号什么时候处理

4.3 内核如何实现信号的捕捉

快速记忆

4.4 sigaction

mask


1.core dump字段

core dump在进程控制中进程等待部分,我们遗留了一个core dump字段,在waitpid中有一个status参数,该参数是一个输出型参数,其中status不能简单的当做整形来看待,我们说要当做位图来看待。

我们关于status只需要关心该整数的低16个比特位。这16个比特位会分为3个部分。次低8位(8-15)存放这子进程的退出码;低7位的作用,我们刚刚说到,代码跑完结果正确,代码跑完,结果不正确,那么代码异常呢?因此低7位的作用就是处理异常。一个进程如果异常退出,是因为这个信号收到了特定的信号!!还有一个位就是第7位也就是core dump字段。

1.1 Core dump是什么?

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

1.2 用代码看看Core Dump

我们使用C++语言写一个最简单的异常错误,查看core dump

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

using namespace std;

int main()
{
    //  fork 创建 child process
    pid_t id = fork();
    if (id == 0)
    {
        // child
        int *p = nullptr;
        *p = 1000;
        //访问野指针

        exit(1); //退出码设置为1
    }

    //父进程阻塞等待
    int status = 0;
    waitpid(id,&status,0);
    printf("exit code:%d, signo : %d , core dump code : %d\n",
        (status>>8) & 0xFF, status & 0x7F, (status >> 7)&0x1);

    return 0;
}

注意:如果你是用云服务器输出的的结果中core dump code为0时,请查看ulimit -a中core file size字段的大小是否为0,如果为0时使用 ulimit -c 1024 即可。再次运行时,core dump code 就会变成1。

当我们运行结束后,再查看当前工作目录下,会发现生成了一个core文件,文件后面的数字是引起core文件的进程是谁。-- 核心转储

核心转储:会把进程在运行中,对应的异常上下文数据,core dump到磁盘上,方便调试。并且如果core dump了会将status的core标志位置1。

1.3 core dump的作用

当程序发生异常终止或者程序运行崩溃时,对应的异常上下文数据会被core dump到磁盘上,方便调试,那么我们如果来查看呢?如何准确的定位呢?这里我们将进行演示,首先我们更改makefile,给程序加上-g调试选项信息。

myproc:myproc.cc
	g++ -o $@ $^ -g -std=c++11

.PHONY:clean
clean:
	rm -f myproc

然后,我们运行该程序(注意,一定要打开core file size 信息)

我们发现已经成功的生成了core 文件,现在我们进入gdb调试阶段之后,输入core-file 【生成的core文件】

按下回车我们发现,程序直接定位到了错误的行数,这样就方便我们定位错误,方便调试。

如果就是core dump的意义和使用方式。

core dump一般会被关掉

为什么core dump一般会被关掉呢?

core dump一般要配合gdb使用,而线上发布的产品一般是release版本,由于不是debug版本,即使我们获得了core文件,也gdb不了。

2.阻塞信号

2.1 信号其他相关常见概念

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

2.2在内核中的表示

我们会在写程序的时候,会无数次的直接或者间接的访问系统级软硬件资源(管理者是OS),本质上你并没有自己去操作这些软硬件资源,而是必须通过操作系统无数次的陷入内核调用内核的代码完成访问的动作,然后把结果返回给用户,用户得到结果。

信号在内核中的表示示意图

  1. 每个信号都有两个标志位分别表示阻塞(Block)和未决(Pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,知道信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

因此通过pending表格和hanlder这两个表,进程就具备了识别和处理信号的能力。由于pending表是一个位图,下标表示几号信号,表中的内容为0表示该信号没有收到,为1表示收到了该信号。因此根据pending表就可以知道进程时候收到了某一信号。如果收到了某一信号,接下来再来看hanlder表格,hanlder是一个函数指针数组,表示的是收到某一个信号后的处理动作。因此这也就具备了处理信号的能力。

3.信号产生中

我们在刚刚了解了pending和hanlder,还有一个block,block是用来阻塞某个信号。block也是一个位图。block位图和pending位图是一模一样的,但是含义是存在不一样的。其中:block和pending位图中第几个比特位代表第几个信号,这是一样的;而不一样的是,位图中第几个比特位的内容表示是不一样的,我们刚说pending位图中比特位的内容表示是否收到信号,而block位图中比特位的内容表示是否阻塞(拦截)该信号。也就是说即使pending收到了该信号,但是被block之后还是不能递达,不能hanlder。

3.1 sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞表示也是这样表示的。因此,未决和阻塞表示可以用相同的数据类型sigset_t来存储。,sigset_t称为信号集。这个类型可以表示每个信号的“有效”或“无效”状态。在阻塞信号中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集合中“有效”和“无效”的含义是该信号是否处于未决状态。

因此我们有了sigset_t在用户层上有了block信号集和pending信号集。而block信号集也叫做信号屏蔽字。

3.2信号集操作函数

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

#include <signal.h>
//对信号集做清空
int sigemptyset(sigset_t *set);
//对信号集全置1
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);

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

3.2.1 sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(block)。

#include <signal.h>
// set是新增,覆盖,删除  oset 通过该参数返回屏蔽字
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返回前,至少将其中一个信号递达。

3.2.2 sigpending

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

这个参数可以拿出来我们pending位图的内容。也可以修改pending表

3.3 使用程序查看pending表

3.3.1手动设置pending表

在一开始所有的信号都不会被block,所有pending都是0,所有hanlder都是default默认的。接下来我们想尝试获取一下当前进程的pending信号集。

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

using namespace std;

static void showPending(sigset_t *pendings)
{
    // 一个信号一个信号判断 如果在就打印1 如果不在打印0
    for(int sig = 1; sig<= 31;sig++)
    {
        if(sigismember(pendings,sig))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout << endl;
}

int main()
{
    //1.尝试不断的获取当前进程的pending信号集
    sigset_t pendings;
    while(true)
    {
        // 1.1 清空信号集
        sigemptyset(&pendings);
        
        // 1.2 获取当前进程的pengding信号集
        if(sigpending(&pendings) == 0 )
        {
            // 1.3 打印一下当前进程的pending信号集
            showPending(&pendings);
        }
        sleep(1);
    }
    return 0;
}

当程序执行起来时,我们发现进程的pending表都是空的,因为我们有一个置零的操作,那么我们如果更形象的看到当前进程的收到某一信号时,pending表对应位置变为1呢?因此我们加入signal函数。并且被block5秒钟,因此一旦2号信号收到时,前5秒是无法被递达,并且保存在pending表中

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

using namespace std;


void hanlder(int signo)
{
    cout<<"我是一个进程,刚刚获取到了一个信号:"<<signo<<endl;
}

static void showPending(sigset_t *pendings)
{
    // 一个信号一个信号判断 如果在就打印1 如果不在打印0
    for(int sig = 1; sig<= 31;sig++)
    {
        if(sigismember(pendings,sig))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout << endl;
}

int main()
{
    //3.屏蔽2号信号
    sigset_t bsig,obsig;
    sigemptyset(&bsig);
    sigemptyset(&obsig);
    //3.1添加2号信号到信号屏蔽字中
    sigaddset(&bsig,2);
    //3.2设置用户级的信号屏蔽字到内核汇总 让当前进程屏蔽到2号信号
    sigprocmask(SIG_SETMASK,&bsig,&obsig);

    //2.signal
    signal(2,hanlder);
    //1.尝试不断的获取当前进程的pending信号集
    sigset_t pendings;
    while(true)
    {
        // 1.1 清空信号集
        sigemptyset(&pendings);
        
        // 1.2 获取当前进程的pengding信号集
        if(sigpending(&pendings) == 0 )
        {
            // 1.3 打印一下当前进程的pending信号集
            showPending(&pendings);
        }
        sleep(1);
    }
    return 0;
}

至此我们可以看到,当把2号信号block后,只能暂时保存在pending表中,因此当我们查看pending表时,2号位置被置为1。那么我们如果把31个信号全部都block呢?会发生什么?

我们发现9号信号是不会被block的,这个我们也了解。9号信号是管理员信号,不能被用户所修改操作。

3.3.2 恢复信号

如果我们想把刚才的信号恢复,一旦恢复就会立马被递达。那么pending信号集也会从1变成0

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

using namespace std;

int cnt = 0;

void hanlder(int signo)
{
    cout<<"我是一个进程,刚刚获取到了一个信号:"<<signo<<cnt<< endl;
}

static void showPending(sigset_t *pendings)
{
    // 一个信号一个信号判断 如果在就打印1 如果不在打印0
    for(int sig = 1; sig<= 31;sig++)
    {
        if(sigismember(pendings,sig))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout << endl;
}

int main()
{
    cout<<"进程pid:"<<getpid()<<endl;
    //3.屏蔽2号信号
    sigset_t bsig,obsig;
    sigemptyset(&bsig);
    sigemptyset(&obsig);
    //3.1添加2号信号到信号屏蔽字中
    for(int sig = 1;sig<=31;sig++)
    {
        sigaddset(&bsig,sig);
        //2.signal
        signal(sig,hanlder);
    }
    //3.2设置用户级的信号屏蔽字到内核汇总 让当前进程屏蔽到2号信号
    sigprocmask(SIG_SETMASK,&bsig,&obsig);

    //1.尝试不断的获取当前进程的pending信号集
    sigset_t pendings;
    while(true)
    {
        // 1.1 清空信号集
        sigemptyset(&pendings);
        
        // 1.2 获取当前进程的pengding信号集
        if(sigpending(&pendings) == 0 )
        {
            // 1.3 打印一下当前进程的pending信号集
            showPending(&pendings);
        }
        sleep(1);
        cnt++;
        if(cnt == 10)
        {
            //结束信号
            cout<<"解除对所有信号的block......" <<endl;
            sigprocmask(SIG_SETMASK,&obsig,nullptr);
        }
    }
    return 0;
}

这是我们一次性把所有的信号都恢复,如果我们指向恢复特定信号(这里用2号信号为例),什么意思?就是当进程运行10秒时,只解除对2号信号的block。看到的现象时只有2号恢复为0,其他唯一,我们可以使用SIG_UNBLOCK。具体代码如下:

这里只把if里面稍加更改即可

        if(cnt == 10)
        {
            //结束信号
            cout<<"解除对2号信号的block......" <<endl;
            sigset_t sigs;
            sigemptyset(&sigs);
            sigaddset(&sigs,2);
            sigprocmask(SIG_UNBLOCK,&sigs,nullptr);
            
            
            
            //cout<<"解除对所有信号的block......" <<endl;
            //sigprocmask(SIG_SETMASK,&obsig,nullptr);
        }

4.信号捕捉

4.1用户级页表和内核级页表

我们在之前学习进程地址空间的时候谈到,一个进程的进程地址空会被分为用户空间(0~3G)和内核空间(3~4G),其中进程地址空间通过页表与物理内存映射,而用户级页表是每一个进程都有一份自己的用户级页表。内核级页表是所有进程共享的,只有一份,前提是你有权利访问!因此无论进程怎么切换,我们都可以找到内核的代码和数据,前提是你只要能够有权利访问! 当前进程如何具备权利,访问这个内核页表,乃至访问内核数据呢?进程如果是用户态的--只能访问用户级页表;进程如果是内核态的--能够访问内核级和用户级页表。而我们怎么知道我是用户态还是内核态呢?CPU内部有对应的状态寄存器CR3,有比特位标识当前进程的状态。其中3表示用户态,0表示内核态。

4.2进程的信号什么时候处理

进程的信号在被合适的时候处理--从内核态返回用户态的时候--检测--处理

  1. 如何理解内核态和用户态
  2. 进程的生命周期中,会有很多次机会去陷入内核(中断,陷阱,系统调用,异常.....),一定会存在很多次的机会进入内核态返回用户态

4.3 内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或者异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检查到信号SIGQUIT递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数。sighandler函数和main函数使用不同的堆栈空间,他们之间不存在调用和被调用的关系。两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达。这次再返回用户态就是恢复main函数的上下文继续执行了。

快速记忆

类似于无穷大,其中与横线有多少个交点,就证明有多少个状态切换,方向决定了是从内核到用户还是用户到内核

4.4 sigaction

#include <signal.h> 
            // signo: 捕捉哪个信号     act:设置成什么动作    oact:之前的动作
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1.signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

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

using namespace std;

int cnt = 0;

void handler(int signo)
{
    cout<<"我是一个进程,刚刚获取到了一个信号:"<<signo<<cnt<< endl;
}

int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(2,&act,&oact);

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

我们发现就可以捕捉2号信号。

mask

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字。这样就保证了在处理某个信号时,如果这个信号再次产生,那么他会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数。(如果正在处理2号信号,又要调用2号信号,就会block,被屏蔽)。

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

using namespace std;

int cnt = 0;

void handler(int signo)
{
    cout<<"我是一个进程,刚刚获取到了一个信号:"<<signo<<"	cnt: "<<cnt<< endl;
    sigset_t pending;
    //增加hanlder信号的时间,永远都会正在处理2号信号!
    while(true)
    {
        cout<<"."<<endl;
        sigpending(&pending);
        for(int i = 1;i<=31;++i)
        {
            if(sigismember(&pending,i))
                cout<<'1';
            else    
                cout<<'0';
        }
        cout<<endl;
        sleep(1);
    }
}

static void showPending(sigset_t *pendings)
{
    // 一个信号一个信号判断 如果在就打印1 如果不在打印0
    for(int sig = 1; sig<= 31;sig++)
    {
        if(sigismember(pendings,sig))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout << endl;
}


int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(2,&act,&oact);

    while(true)
    {
        cout<<"main running"<<endl;
        sleep(1);
    }

    return 0;
}

(本篇完)

附录:

makefile

# .PHONY:all
# all:mykill mypro

# mykill:mykill.cc
# 	g++ -o $@ $^ -std=c++11
myproc:myproc.cc
	g++ -o $@ $^ -g -std=c++11

.PHONY:clean
clean:
	rm -f myproc

myproc.cc

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

using namespace std;

int cnt = 0;

void handler(int signo)
{
    cout<<"我是一个进程,刚刚获取到了一个信号:"<<signo<<"	cnt: "<<cnt<< endl;
    sigset_t pending;
    //增加hanlder信号的时间,永远都会正在处理2号信号!
    while(true)
    {
        cout<<"."<<endl;
        sigpending(&pending);
        for(int i = 1;i<=31;++i)
        {
            if(sigismember(&pending,i))
                cout<<'1';
            else    
                cout<<'0';
        }
        cout<<endl;
        sleep(1);
    }
}

static void showPending(sigset_t *pendings)
{
    // 一个信号一个信号判断 如果在就打印1 如果不在打印0
    for(int sig = 1; sig<= 31;sig++)
    {
        if(sigismember(pendings,sig))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout << endl;
}


int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(2,&act,&oact);

    while(true)
    {
        cout<<"main running"<<endl;
        sleep(1);
    }

    return 0;
}


// int main()
// {
//     cout<<"进程pid:"<<getpid()<<endl;
//     //3.屏蔽2号信号
//     sigset_t bsig,obsig;
//     sigemptyset(&bsig);
//     sigemptyset(&obsig);
//     //3.1添加2号信号到信号屏蔽字中
//     for(int sig = 1;sig<=31;sig++)
//     {
//         sigaddset(&bsig,sig);
//         //2.signal
//         signal(sig,hanlder);
//     }
//     //3.2设置用户级的信号屏蔽字到内核汇总 让当前进程屏蔽到2号信号
//     sigprocmask(SIG_SETMASK,&bsig,&obsig);

//     //1.尝试不断的获取当前进程的pending信号集
//     sigset_t pendings;
//     while(true)
//     {
//         // 1.1 清空信号集
//         sigemptyset(&pendings);
        
//         // 1.2 获取当前进程的pengding信号集
//         if(sigpending(&pendings) == 0 )
//         {
//             // 1.3 打印一下当前进程的pending信号集
//             showPending(&pendings);
//         }
//         sleep(1);
//         cnt++;
//         if(cnt == 10)
//         {
//             //结束信号
//             cout<<"解除对2号信号的block......" <<endl;
//             sigset_t sigs;
//             sigemptyset(&sigs);
//             sigaddset(&sigs,2);
//             sigprocmask(SIG_UNBLOCK,&sigs,nullptr);
            
            
            
//             //cout<<"解除对所有信号的block......" <<endl;
//             //sigprocmask(SIG_SETMASK,&obsig,nullptr);
//         }
//     }
//     return 0;
// }

// int main()
// {
//     //  fork 创建 child process
//     pid_t id = fork();
//     if (id == 0)
//     {
//         // child
//         int *p = nullptr;
//         *p = 1000;
//         //访问野指针

//         exit(1); //退出码设置为1
//     }

//     //父进程阻塞等待
//     int status = 0;
//     waitpid(id,&status,0);
//     printf("exit code:%d, signo : %d , core dump code : %d\n",
//         (status>>8) & 0xFF, status & 0x7F, (status >> 7)&0x1);

//     return 0;
// }



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

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

相关文章

Carla学习2:carla安装与使用

文章目录0. 建议1. carla学习相关链接1.1 官方资料1.1 学习教程2. Carla安装2.1 服务器端2.1.1 下载预编译版本&#xff08;也可以使用下载源码并编译&#xff09;2.1.2 启动服务器端及服务器端显示导航2.3 客户端2.3.1 创建python环境2.3.2 安装carla 的pythonAPI所需要的依赖…

自动平移门风淋室——化妆品行业的全面自动化门体

自动平移门风淋室工作原理&#xff1a;自动平移门风淋室包括单人风淋室,双人风淋室,多人风淋室,全不锈钢风淋室,快速卷帘门风淋室,防爆风淋室,风淋通道、转角风淋室、钢板烤漆风淋室, QS认证风淋室,全自动风淋室,臭氧杀菌风淋室,电加热风淋室,防静电风淋室,化妆品行业风淋室,汽…

一万五字的文章,超详细的画图,带你理解链表的基础和进阶题目(含快慢指针的讲解)

在今天的文章中&#xff0c;我将带来链表的面试题。在数据结构的学习过程中&#xff0c;画图是尤为重要的&#xff0c;所以在这些题目的讲解的过程中&#xff0c;我以画图为主。温馨提示:由于图片过大&#xff0c;手机观看可能出现模糊不清的情况&#xff0c;建议在电脑观看该篇…

Redis【10】-Redis发布订阅

简介 Redis 发布订阅(pub/sub)是一种消息通信模式&#xff1a;发送者(pub)发送消息&#xff0c;订阅者(sub)接收消息。 Redis 客 户端可以订阅任意数量的频道。 Redis 发布订阅(pub/sub)是一种消息通信模式&#xff1a;发送者(pub)发送消息&#xff0c;订阅者(sub)接收消息。 R…

R-CNN系列目标检测算法对比

引言 对比了R-CNN&#xff0c;Fast R-CNN&#xff0c;Faster R-CNN&#xff0c;Mask R-CNN目标检测算法的发展过程与优缺点。 R-CNN R-CNN是第一个成功第将深度学习应用到目标检测的算法。后面的Fast R-CNN&#xff0c;Faster R-CNN都是建立在R-CNN的基础上的。 R-CNN的检测…

实验2_前馈神经网络实验

文章目录实验要求数据集定义1 手动实现前馈神经网络解决上述回归、二分类、多分类任务1.1手动实现前馈网络-回归任务1.2 手动实现前馈网络-二分类任务1.3 手动实现前馈网络-多分类1.4 实验结果分析2 利用torch.nn实现前馈神经网络解决上述回归、二分类、多分类任务2.1 torch.nn…

[附源码]Node.js计算机毕业设计宠物短期寄养平台Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我们…

Mybatis 基础入门示例-步骤清晰简单

目录 1、新建数据库 2、新建项目 2.1导入依赖 2.2创建子工程&#xff08;新建模块&#xff09; 2.3添加配置文件mybatis-config.xml 2.4添加数据源 2.5编写mybatis核心配置文件 2.6编写MybatisUtils工具类 3、编写代码 3.1实体类 3.2 Mapper(UserDao)接口 3.3 接口…

如何在 Hexo Blog 网站上添加图标(iconfont 使用)

emsp; 因为在制作自己的个人主页的时候遇到了Hexo主题没有提供对应图标的问题&#xff0c;就查看了一下Hexo主题是如何添加图标的。发现主要的方法是直接修改fonts文件夹下的iconfont.svg文件。修改yilia theme下的font文件&#xff0c;这个也刚好是同学blog使用的主题&#x…

代码是如何控制硬件的?

简单来说&#xff0c;就是软件指令通过操作寄存器&#xff0c;控制与、或、非门搭建的芯片电路&#xff0c;产生、保存高低电平信号&#xff0c;实现相应的逻辑&#xff0c;最终通过IO、串口等输出。 要想更清楚的了解软件控制硬件的原理&#xff0c;就要明白cpu的框架及工作原…

Mysql 进阶(面向面试篇)索引

1、索引 1.1 索引概述 索引&#xff08;index&#xff09;是帮助MySQL高效获取数据的数据结构(有序)。在数据之外&#xff0c;数据库系统还维护着满足 特定查找算法的数据结构&#xff0c;这些数据结构以某种方式引用&#xff08;指向&#xff09;数据&#xff0c; 这样就可以…

springboot整合Swagger在线文档

SpringBoot整合Swagger2在线文档 一 什么是swagger&#xff1f; 我们前面有讲到说开发时会创建Restful风格的API接口&#xff0c;供第三方或前端人员使用&#xff0c;那么前端人员在使用的过程中怎么知道有哪些接口呢。这个时候可以通过写接口文档来解决&#xff0c;但不同的…

202/12/10 基础算法每日5道详解

21. Merge Two Sorted Lists合并两个排序列表 You are given the heads of two sorted linked lists list1 and list2. Merge the two lists in a one sorted list. The list should be made by splicing together the nodes of the first two lists. Return the head of the m…

Java基于springboot的人职匹配推荐系统-计算机毕业设计

项目介绍 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于人职匹配推荐系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了人职匹配推荐系统&#xff0c;它彻底改变…

[附源码]计算机毕业设计基于人脸识别的社区防疫管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

字节管理薪资被应届生倒挂7K,这真的是不把老员工当人?

一位字节跳动的小管理爆出&#xff0c;无意中看到了整个部门薪资&#xff0c;本以为自己算比较高的&#xff0c;但看完之后整个人都傻眼了。小组长的职位月薪28K&#xff0c;而手下组员却是35K&#xff0c;当天晚上抽了一包烟也没想明白是为什么。 楼主表示&#xff0c;自己是字…

算法基础篇-05-排序-LowB三人组(冒泡/选择/插入排序)

1. LowB 三人组介绍 LowB 三人组的时间复杂度都是 O(n^2) 1.1 冒泡排序(Bubble Sort) 列表每2个相邻的数&#xff0c;如果前面比后面大&#xff0c;则交换这两个数。一趟排序完成后&#xff0c;则无序区减少一个数&#xff0c;有序区增加一个数&#xff1b;时间复杂度 O(n^2…

Linux 伙伴系统

Linux 伙伴系统前言一、rmqueue1.1 流程图1.2 函数原型1.3 通过PCP分配1.4 大阶页面分配二、__rmqueue2.1 流程图三、__rmqueue_pcplist3.1 流程图四、__rmqueue_fallback五、__rmqueue_smallest5.1 源码5.1.1 get_page_from_free_area5.1.2 del_page_from_free_list5.1.3 expe…

从零开始把 SpringBoot 搬到 K8s 上运行,我用了这几步!

前言 大家好&#xff0c;我是网管。咱们的 K8s 入门和实践&#xff0c;在经历了三篇理论知识的后&#xff0c;相信各位都已经期待许久&#xff08;可能的吧&#xff09;&#xff0c;就差私信我&#xff0c;你整着理论整半天有啥用&#xff0c;本大人写的程序怎么能放到 K8s 上运…

Istio初探

Istio初探 前置环境&#xff1a;docker 一、安装k8s&#xff1a; ● https://segmentfault.com/a/1190000042204035 1、 https://github.com/gotok8s/k8s-docker-desktop-for-mac.git 2、 https://github.com/kubernetes/dashboard 3、 获取token curl ‘http://127.0.0.1:80…