Linux信号的保存与信号的处理

news2024/11/26 19:31:39

目录

前言

一、信号保存

1、重谈信号的概念

2、信号在内核中的表示

3、sigset_t

4、信号集操作函数、

• sigset_t相关的接口

• sigpromask

• sigpending

二、信号处理

1、再谈地址空间

2、用户态与内核态

• 内核态和用户态的切换

• 用户态切换为内核态的几种情况

3、信号的捕捉时机

4、sigaction

三、扩展知识

1、可重入函数

2、volatile

3、SIGCHLD

4、浅谈键盘输入数据的过程

5、浅理解OS是如何正常运行的

• 如何理解系统调用

• OS 是如何运行的


前言

上一期就介绍了,信号产生后可能并不会立即的执行,而是等到合适的时候去执行!这样就意味着,在被执行之前需要将信号给保存起来!我们上一期只是很粗糙的说他是用一张位图保存的,本期我们将详细的介绍保存数据信号的数据结构!

一、信号保存

上一期我么已经对信号的产生做了介绍,并了解了信号的生命周期有了整体的认识:信号的产生 -> 信号的保存 -> 信号的处理!我们下面在此基础上,先来对一些概念进行校正!

1、重谈信号的概念

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

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

• 使信号处于"停滞"状态,无论是否有信号产生,都无法进行递达的状态称为信号阻塞        Block)

• 进程是可以选择阻塞某个信号的!

通俗的解释就是,递达就是实际执行信号对应的handler方法;未决就是信号产生了但是还没有处理的那种状态;阻塞就是不挂你有没有这个信号,我先把这个信号给拉黑,不让你递达!

下面用一个例子理解:

午饭时间到了,你妈给你打电话通知你要吃饭了(信号产生),你收到通知去吃饭的路上这个过程就是信号未决;但是现在不巧,你妈刚刚打电话之前,你开了一把CF,你收到你妈的通知后,你并没有立刻去吃饭,而是先打完CF再去吃饭,此时你将你妈的通知"停滞"往后了,此时你妈的通知就是信号的阻塞!你怕你妈又打天花催你,于是你就把你妈给拉黑了,这属于还没有收到信号,就把信号给阻塞了!

注意:

信号的阻塞可以发生在递达前的任意时候

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

• 阻塞和忽略是不一样的;阻塞是可能收到了信号但是干不了即不能递达,而忽略是收到信号的一   种处理方式,只不过这种处理方式是啥都不干

2、信号在内核中的表示

对于一个信号来说,无非需要存储三种状态:

1、信号是否阻塞 

2、信号是否未决

3、信号递达时的执行动作

所以在内核中,每个进程都维护了三张表:block 表、pending 表、handler

如图,block ,表示信号是否被阻塞 pending ,表示是否未决 handler ,表示是否递达;

其中,block  pending 表,其实是两张位图!31个普通信号正好可以用一个int来表示!其中,位图的每一位表示对应的信号,里面的值 0/1 表示是否阻塞/未决

而,handler 表,其实是一个函数指针数组!其中数组的下标表示的是对应的信号,数组中的元素表示的是该信号的递达方法

OK,介绍到这里我们也就可以明白,我们上一期使用signal(int signum, handler);就是通过signum找到handler表,将自定义的处理函数的地址放进去,然后在执行的时候我们就可以使用自己的递达方法了!

3、sigset_t

根据上面的介绍,每个信号只有一个bit的未决标志,不记录该信号产生了多少次,其中阻塞也是一样的!因此,未决和阻塞的表示可以用相同的数据类型sigset_t来存储,sigset_t称为信号集

sigset_t类型可以表示每个信号对应的"有效"和"无效";其中阻塞信号集中,有效就是阻塞,无效就是非阻塞;未决信号集中,有效就是有信号,无效就是没有收到信号!

阻塞信号集也叫做当前进程的信号屏蔽字(signal mask),和以前的权限掩码(权限屏蔽字)一样!

这就是sigset_t的定义

#ifndef ____sigset_t_defined
#define ____sigset_t_defined

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

#endif

4、信号集操作函数、

• sigset_t相关的接口

对信号集的操作其实就是对 block pending 两张表的 增删查改!

上面刚介绍了,他两本质就是位图,你想操作你可以直接用一个变量获取,然后自己使用各种位运算操作!理论上是没有问题的,但是我们不推荐这样,而且OS也不同意让你操作,因为block pending 是进程PCB中的字段,除了OS谁也操作不了!所以,OS就提供了一批操作他两的系统调用!

#include <signal.h>

int sigemptyset(sigset_t *set);	//初始化信号集set,全部置0
int sigfillset(sigset_t *set);	//将信号集set,全部置1
int sigaddset(sigset_t *set, int signum);	//将signum信号增加到set
int sigdelset(sigset_t *set, int signum);	//将signum信号从set中删除
int sigismember(const sigset_t *set, int signum);	// 判断signum信号是否在set中

这批函数的返回值都是,成功,返回0; 失败返回-1

小Tips:在创建信号集, sigset_t的变量后,需要使用sigemptyset 进行做初始化操作,保证信号集的合法性!

• sigpromask

作用:调用 sigpromask 可以读取或更改进程的信号屏蔽字/阻塞信号集(block)

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

返回值

成功,返回0;失败, 返回-1, errno被设置

参数

1、参数1:how,对屏蔽字的更改操作

       SIG_BLOCK :set包含了我们希望添加到屏蔽字的信号,相当于mask=mask|set

       SIG_UNBLOCK :set中包含了我们希望从当前的信号屏蔽字中删除的阻塞信号,相当

                                     mask=mask & ~set

       SIG_SETMASK : 设置当前的信号屏蔽字为set,相当于mask = set

2、参数2: set, 对屏蔽字的内容更改  

3、参数3: 获取原先没有被修改的屏蔽字内容,目的是为了恢复

• sigpending

作用:获取未决信号集

参数

输出型参数,获取到的pending 表的值

返回值

成功,返回0;失败, 返回-1, errno被设置

OK,我们可以阻塞2号信号,然后再给他发2号信号,看两点现象:1、当发送2号信号时,没有终止 2、当检测其pending表时,我们发现当我们发送完2号信号后对应的位机会从0 变 1

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

void PrintPending(sigset_t &pending)
{
    std::cout << "cur process[" << getpid() << "]pending! ";
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(&pending, i))
        {
            std::cout << '1';
        }
        else
        {
            std::cout << '0';
        }
    }
    std::cout << std::endl;
}

int main()
{
    // 1、创还能sigset_t
    sigset_t block_set, old_set;
    // 2、初始化
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    // 3、将2号信号给设置到block_set
    sigaddset(&block_set, 2);
    // 4、将block_set的数据写到block表
    sigprocmask(SIG_BLOCK, &block_set, &old_set);

    while (true)
    {
        // 5、获取pending表
        sigset_t pending;
        sigpending(&pending);

        // 隔一秒打印一次pending
        PrintPending(pending);
        sleep(1);
    }

    return 0;
}

上面的结果我们看到,首先2好信号已经不起作用了,因为2号被阻塞了,一旦被阻塞,就不再执行对应的handler方法了,阻塞收到信号后会在pending表中记录,上面也看到了由0到1!

当解除阻塞后,该进程会立马去执行,信号对应的handler方法!如何解除?我们之前不是保存了原先的block表吗,可以将old_set覆盖当前修改过的block:

2号信号的默认就是终止进程,所以这里解除之后就直接终止了!

如果你想看到他变成再由1变0,你可以捕捉:

当然,这里还可以验证一下,我们在执行handler前对pending位图的对应位进行修改,但是在执行前修改,还是执行后修改呢?其实很好验证:

void handler(int sig)
{
    std::cout << sig << "号信号被递达了...." << std::endl;
    std::cout << "-------------------------------" << std::endl;
    sigset_t pending;
    sigpending(&pending);
    PrintPending(pending);
    std::cout << "-------------------------------" << std::endl;
}

因为这个handler方法执行完就是递达结束了!如果执行完没有变0就是执行完再修改的,反之如果在没执行完handler方法就变0了,就是在执行前修改的!

说明在执行handler方法前就修改了!


二、信号处理

前面介绍了,进程收到了信号后,可能不会立即处理,而是在合适的时间处理!现在的问题是到底什么时候才算是合适的时候呢?换句话说就是OS是在什么时候去处理pending中已经收到信号呢?

在正式回答这个问题前,我们先来介绍一个前置知识:用户态和内核态

1、再谈地址空间

Linux操作系统是一个多用户、多任务的操作系统,为了安全性和资源管理,它将系统划分为用户态内核态两种模式运行!(注意:用户态和内核态是针对CPU的

每一个进程在都会有自己独立的虚拟的地址空间,但是我们注意到虚拟地址空间分为两部分用户空间内核空间,用户空间是给普通进程用的,那内核空间呢?当然是给OS用的!

操作系统也是软件,他启动起来也是进程;是进程就有代码和数据!它的代码和数据也和普通进程一样,会通过内核级页表映射到每个进程虚拟地址空间中的内核空间!

• 注意:OS中有很多的进程,但是并不是每个进程启动时,都要为其从磁盘加载一分内核的那部分OS的代码和数据!而是在内存中只有一份,内核级的页表也只有一份多个进程只是在他们各自的内核空间中映射了同一份内核级的页表! 

由于每个进程有独立的地址空间,所以每个进程都可以看到并访问操作系统!而访问OS的本质就是,特定情况下CPU跳转到当前进程地址空间的内核区访问!所以,内核空间的意义在于,无论哪一个进程在调度,随时都可以找到OS!

注意:虽然你每一个进程都可以看到OS,但是并不意味着你可以随意的访问!


2、用户态与内核态

• 概念

根据上面的介绍,我们可以简单的理解用户态和内核态为:

注意:用户态和内核态是针对CPU的!

用户态:CPU执行用户空间代码和数据的状态;

内核态:CPU执行内核空间代码和数据的状态;

• 权限与特点

用户态的权限较低,只能访问受限制的系统资源,无法直接访问硬件等!

内核态的权限最高,拥有访问所有软硬件资源的权限!

用户态与内核态的区别主要如下:

特性用户态内核态
权限受限全权
资源访问有限所有
代码执行用户程序操作系统内核
硬件访问受限可直接访问
安全性较高较低

• 内核态和用户态的切换

我当前进程代代码中使用了系统调用,当该进程被CPU调度起来时,执行到系统调用,此时CPU会保存该进程的上下文,然后修改内部的寄存器例如 ecs/psw 等的标记位,从用户态切换为内核态, 此时CPU有最高的权限,就可以去当前进程地址空间中的内核区执行系统调用的代码了!执行完内核态的代码,此时CPU再一次修改内部寄存器 ecs/psw 的标记位,切换为用户态!然后将在切换到内核前保存的CPU上下文数据恢复,继续执行用户态的代码!

总结:用户切换到内核,前首先保存当前CPU的上下文,然后修改寄存器变成内核态,执行完内核代码,修改CPU内寄存器的标记位,恢复切换前的CPU上下文,继续执行用户态!

• 用户态切换为内核态的几种情况

1、系统调用(System Call)

        • 用户执行一些需要内核权限的操作,例如:读写文件、创建进程、访问网络等;

2、硬件中断 (Hardware Interrupt)

        • 当硬件设备(例如磁盘、网络接口、键盘等)发生中断时,会触发硬件中断,将控制权从用户态转移到内核态。内核会根据中断类型进行处理,并可能需要调用相应的驱动程序来处理硬件事件。

3、时钟中断 (Clock Interrupt)

内核会设置定时器,定期触发时钟中断,用于执行一些周期性任务,例如进程调度、内存管理等。

4、异常(Exception)

 异常是指CPU在执行运行在用户态下的程序时,发生了某些事先不可知的错误或异常情况,如缺页异常、算术异常(如整数除零)、非法指令等。

注意:不仅是上述的三种情况才会变成内核态,而是只要你需要操作系统提供的服务,都会切换进入到内核态!

其中:

• 将用户态切换为内核态称为 陷入内核

• 将内核态切换为用户态称为 返回用户态

陷入内核是一个非常频繁的操作,操作系统会不断地进行用户态和内核态之间的切换,以保证系统的正常运行。

3、信号的捕捉时机

上面哔哔了半天的内核态和用户态,他和信号有嘛关系呢??

其实:信号的捕捉时机就是发生在 内核态 切换为 用户态 之前!

它的执行图如下:

如果在切换回用户态前,检测发现是有信号的,并且信号处理的函数是默认/忽略,此时内核态会执行完默认的方法,然后直接切换回用户态!

上面的执行完默认处理函数很好理解,但是,这里

1、为什么使用户自定义的处理方式时,为什么要切回用户态?

其实原因很简单,不切换用户态在技术角度肯定可以做到!但是如果不切换成用户态,此时OS不知道你自定的处理方法干了啥,万一你是 rm -rf /* 呢?所以如果直接用内核态执行,可能会被用户利用内核态的高权限"为所欲为"!所以,要从内核态切换为用户态,你用户的代码用户执行!这样最起码保证OS的安全!

2、在信号检测时,在做啥?

在检测信号时,其实就是在检查PCB中的那三张表:blockpendinghandler

首先,检查 pending 的每一位,看是否是1;如果是1, 再看 block表,如果是1,就代表阻塞,如果是0,就去看handler 是默认还是自定义;自定义就去用户态执行,自定义就以内核态的身份执行完了,切换回用户态继续往下执行!

3、为什么执行完用户自定义的处理方法后,需要切换回内核在切换回用户继续执行呢?

因为handler是内核的系统调用,调的!和在用户态的主执行流没有直接的调用关系,所以执行完用户自定义的方法后是没有办法回到主执行流的!但是内核是知道的,所以当只想完用户的handler后,通过sigreturn 的特殊系统调用回到内核,然后在通过sys_sigreturn 返回用户,继续向下执行主控制流!

4、sigaction

捕捉信号除了signal 还可以使用sigaction对信号进行捕捉!

OS提供这个系统调用的目的是为了让我们可以在处理信号时,自定义哪些信号要被阻塞!

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数

第一个 signum 是指定哪个信号要被自定义处理。

第二和第三都是 struct sigaction 类型的结构体变量,先来看看这个结构体:

struct sigaction 
{
	void     (*sa_handler)(int);	//自定义动作
	void     (*sa_sigaction)(int, siginfo_t *, void *);	//实时信号相关,不用管
	sigset_t   sa_mask;	//待屏蔽的信号集
	int        sa_flags;	//一些选项,一般设为 0
	void     (*sa_restorer)(void);	//实时信号相关,不用管
};

这个结构体中,我们只需要关心,第一个参数,他是我们自己指定处理signum信号的处理方法!

sa_mask 是屏蔽哪些信号,用户自定义完成; 

sa_flags 一般设置为0, 其他都是和实时信号有关系的,这里不管!

所以,我们如果有需求,可以将sa_mask屏蔽掉一批信号,然后执行自己自定义的那一个,这样可以避免其他信号对signum的干扰:

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

void Print(sigset_t &pending)
{
    for (int sig = 31; sig > 0; sig--)
    {
        if (sigismember(&pending, sig))
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << std::endl;
}

void handler(int signum)
{
    std::cout << "get a sig: " << signum << std::endl;
    while (true)
    {
        sigset_t pending;
        sigpending(&pending);

        Print(pending);
        sleep(1);
    }
}

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

    sigaction(2, &act, &oact);

    while (true)
    {
        std::cout << "I am a process, pid: " << getpid() << std::endl;
        sleep(1);
    }

    return 0;
}

此时由于2好信号没有结束,当再次收到其他信号时,会先屏蔽起来,所以发2号和3号信号都会被阻塞,上面也看到了!当该信号阻塞结束后,才会取消对其他信号的屏蔽!

注意:31个普通信号只有少部分可以被屏蔽,其他的不能屏蔽!

三、扩展知识

1、可重入函数

可重入函数可以简单的理解为,可以被重新进入的函数!

比如单链表的头插场景中,节点node1还未完成插入时,假设刚执行了一步,此时信号被捕捉了,而且处理方式还是自定义的,而且处理方法是将node2也进行头插,此时先执行的是node2的头插,当给node2执行完之后,会在回执行node1的头插,此时会导致node2内存泄漏

此时导致内存泄漏的本质是,node1和node2在操作时同时并发访问了同一个单链表,且对这个单链表没有做任何的保护!因此在并发时就出现可重入导致的内存泄漏,此时的单链表就是临界资源!

我们以前学过的90%的函数都不是可重入函数!

不可重入的条件:

  • 调用了内存管理相关函数
  • 调用了标准 I/O 库函数,因为其中很多实现都以不可重入的方式使用数据结构

2、volatile

volatile是C/C++的一个关键字,它的作用是避免编译器的优化,保证内存的可见性!

我我们举一个栗子:

第一步,我们先使用一个全局变量设计一个死循环的场景,在此之前对2号信号进行自定义捕捉,在自定义捕捉的函数体内,实现将flag赋值为1!

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

int flag = 0;   // 一开始为假

void handler(int signo)
{
    printf("%d号信号已经成功发出了\n", signo);
    flag = 1;
}

int main()
{
    signal(2, handler);

    while(!flag);   // 故意不写 while 的代码块 { }

    printf("进程已退出\n");

    return 0;
}

OK,没问题,符合预期!我们知道编译器会优化,g++一般有个优化等级:

man gcc
/O1

如果你不指定就是O0, 我们这里就优化的小一点O1:

我们发现哎不就是优化了一下吗,这咋就不行了呢?即使疯狂的发送2号信号也不结束了!

其实,这就是编译器把我们的代码给优化了!

我们一般的代码的数据,首先会加载到内存,然后CPU调度时将数据加载到寄存器,这样做是没问题的!但是现在我一优化,编译器一看主函数你就没有对flag做处理,且只有你用了falg!(不用handler十不调的),所以此时会将flag直接设置进寄存器里面:

等到后面信号处理时,即使修改了flag,也不会同步到寄存器了!所以此时主函数那个循环一直就是0你发2好信号他就收到一次,但是就是不结束!

如何解决这个问题呢?将Volatile将在全局的flag前,就可以避免了:

此时就OK了!

3、SIGCHLD

前面在介绍进程等待的时候,介绍过为了避免子进程僵尸,父进程是要以阻塞或者非阻塞轮询的方式等待子进程的退出的

现在的问题是:父进程如何知道子进程退出了呢?

其实,子进程再退出的时候,会给父进程发送 SIGCHLD 的信号!该信号的默认动作时 忽略!

所以,我们可以不再是阻塞是的等到他,而是可以把 SIGCHLD 信号,给捕捉了,让他在自定义的函数体内进行等待!

我们举个例子:让子进程三秒后直接退出,用信号等待:

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

void handler(int sig)
{
    std::cout << "get a signal: " << sig << " pid: " << getpid() << std::endl;
    pid_t rid = waitpid(-1, nullptr, 0); // 阻塞式等待
    if (rid > 0)
    {
        std::cout << "wait child success, rid: " << rid << std::endl;
    }
}

void DoOtherThing()
{
    std::cout << "DoOtherThing()~" << std::endl;
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if (id == 0)
    {
        // child
        std::cout << "I am child process, pid: " << getpid() << std::endl;
        sleep(3);
        exit(1);
    }

    // father
    while (true)
    {
        DoOtherThing();
        sleep(1);
    }

    return 0;
}

没问题!可以等待成功!

可是现在又有问题,你上面是等待了任意一个子进程,且退出的只有一个!那我10个子进程同时退出呢?

我们可以的自定义的方法里面,循环的等待:

那我如果,10个子进程,5个退出5个时钟不退呢?上面这样的代码不就是,一直阻塞了吗?

其实,了可以在处理信号的函数里将其设置为,不要hang住:

void handler(int sig)
{
    std::cout << "get a signal: " << sig << " pid: " << getpid() << std::endl;
    while (true)
    {
        pid_t rid = waitpid(-1, nullptr, WNOHANG); // 非阻塞式等待
        if (rid > 0)
        {
            std::cout << "wait child success, rid: " << rid << std::endl;
        }
        else if (rid < 0)
        {
            std::cout << "wait child success done " << std::endl;
            break;
        }
        else
        {
            std::cout << "wait child success done " << std::endl;
            break;
        }
    }
}

我现在就是不想产生僵尸,又不想等,你结束了就自己退回吧,咋办呢? 

事实上 , 由于 UNIX 的历史原因 , 要想不产生僵尸进程还有另外一种办法 : 父进程调用 sigaction SIGCHLD 的处理动作置为SIG_IGN, 这样 fork 出来的子进程在终止时会自动清理掉 , 不会产生僵尸进程 , 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略通常是没有区别的 , 但这是一个特例。此方法对于 Linux 可用 , 但不保证在其它UNIX 系统上都可用。请编写程序验证这样做不会产生僵尸进程。

4、浅谈键盘输入数据的过程

我们前面介绍过,键盘等外设的数据都是由操作系统调用他们各自的驱动程序读取的!这是我们以前介绍的!但是这样说太过于笼统了,我下面介绍一下它的大概流程!

我们前面一直说的数CPU和外设在数据层面上不打交道,可没说在控制上也不打交道!键盘等外设都有一个自己的中断号,当键盘输入数据时,会由 8259等类似的芯片通过总线(USB等)给CPU发送中断,CPU收到中断后,会读取键盘的中断号存在内部的寄存器里面, 并保存当前任务的上下文,OS在一开始加载的时候会在内存加载一张函数指针数组的表,也称中断向量表!他里面每个元素都是提前设计好的,例如:读磁盘、读网卡、读键盘、等!上述的外设的中断号可以简单的理解为该中断向量表的下标!CPU就会拿着这个中断号,在中断向量表中索引,当找到中断号对应的下标,就去调度OS执行对应的方法!当执行完后,会执行一条中断返回指令,继续执行原来任务!

介绍完这个东西,你可能会觉得这不就是 和我们的信号一样吗?是的!但是,是先有的中断,信号是模拟中断产生的!信号是纯软件,中断是软件+硬件!

5、浅理解OS是如何正常运行的

• 如何理解系统调用

系统调用的本质是一张函数指针数组!它里面就是所有系统调用的函数名!我们平时的调用系统调用时,本质底层是CPU拿着系统调用号,到OS的系统调用的函数指针数组执行相关的方法!

这个系统调用号,从哪里来?

其实当你执行系统调用时,它的内部一定会将相对应的系统调用号,move到相关的寄存器,然后通过0x80等发生硬件中断,让CPU保存上下文,然后切换为内核态,按照寄存器里面的值索引到相关的方法,然后执行!

当然真实的系统调用比这复杂的多,我们简单的这样理解一下即可!

• OS 是如何运行的

操作系统的本质就是一个死循环+时钟中断, 不停的调度系统的任务!

外部的硬件时钟,会隔一定的时间(很短)相CPU发送时钟中断,CPU收到时钟中断后,获取中断号,然后检查当前任务的时间片,如果任务的时间片没有结束,继续执行该任务,如果时间片结束了,切换为内核态,按照中断号找到中断向量表中的对应方法即调度其OS的其他任务!

当然这是最简单的理解,真实的比这个复杂的多!!!

OK,本期分享就到这里!好兄弟,我是cp我们下期再见!

结束语:愿写尽代码千行,头发依旧如当初模样!

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

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

相关文章

软件开发的航海之旅:程序员的实用神器

在软件开发的海洋中&#xff0c;程序员们就像勇敢的航海家&#xff0c;驾驭着代码的航船&#xff0c;向着未知的目的地进发。在这个过程中&#xff0c;各种实用的开发工具就如同航海中的指南针&#xff0c;帮助他们导航、加速开发、优化代码质量&#xff0c;并最终抵达成功的彼…

YOLO-V3

一、概述 最大的改进就是网络结构&#xff0c;使其更适合小目标检测特征做的更细致&#xff0c;融入多持续特征图信息来预测不同规格物体先验框更丰富了&#xff0c;3种scale&#xff0c;每种3个规格&#xff0c;一共9种softmax改进&#xff0c;预测多标签任务 先验框&#xf…

dubbo:dubbo+nacos整合springcloud gateway实现网关(三)

文章目录 0. 引言1. 集成gateway网关1.1 实操步骤1.2 dubbo提供者注册到nacos出现两个实例的问题 2. 源码3. 总结 0. 引言 上次我们讲到使用zookeeper作为注册中心搭建dubbo微服务框架&#xff0c;但是我们还缺少一个服务总入口&#xff0c;也就是我们的网关服务。所以今天我们…

小兔鲜首页制作css

一、项目目录 项目名-客户端 xtx-pc 1.images&#xff1a;存放固定使用的图片&#xff0c;例如&#xff1a;logo、样式修饰图 2.uploads:存放非固定图片&#xff0c;例如&#xff1a;商品图、宣传图等需要上传的图片 3.iconfont:字体图标素材 4.css文件&#xff1a; 4.1base.cs…

猫咪除毛除臭大作战!宠物空气净化器帮助铲屎官轻松应对换毛季

最近天气真的好热&#xff0c;可以的话真想呆在空调房一直不出来&#xff0c;人都受不了&#xff0c;何况是一身厚重毛发的猫咪。这不&#xff0c;本该在春季结束的换毛季&#xff0c;延续到了夏天。它就像一个行走的蒲公英&#xff0c;家里到处散落着它的毛发。体臭和便臭在密…

【软件测试·研究向】Major 变异测试工具使用教程

目录 前言 1 安装 1.1 下载解压 1.2 配置环境变量 1.3 入门示例脚本 2 Mutator Plugin 编译器插件 2.1 如何使用插件&#xff1f; 2.2 插件配置选项 2.3 Major 支持的变异算子 2.4 突变体的日志记录 2.5 生成突变体的源代码 3 构建系统集成 4 Major 突变语言 (M…

系统架构设计师:信息系统概述及分类

信息系统是由计算机硬件、网络和通信设备、计算机软件、信息资源、信息用户和规章制度组成的以处理信息流为目的的人机一体化系统。 从技术上可以定义为一系列支持决策和控制的相关要素&#xff0c;这些要素主要包括信息的收集、检索、加工处理和信息服务。除了支持决策、协作…

【Python】动态类型、输入和输出、条件语句

动态类型 程序运行过程中&#xff0c;变量的类型可能会发生改变 a hello print(a) a 10 print(a)#运行结果 hello 10在程序执行过程中&#xff0c;a 的类型刚开始是 int&#xff0c;后面变成了 str C / Java 这样的语言则不允许这样的操作&#xff0c;一个变量定义后类…

Qt第十七章 多线程

文章目录 多线程1. 线程概念的起源2. 三种方式创建线程3. 启动线程前的准备工作4. 启动线程/退出线程5. 操作运行中的线程6. 为每个线程提供独立数据7.子线程不能操作ui解决方案 多线程 1. 线程概念的起源 单核CPU 早期还没有线程的概念&#xff0c;如何保证2个进程同时进行呢…

BaseCTF 高校联合新生赛Week1(web)

目录 HTTP 是什么呀 喵喵喵•&#xfecc;•​编辑 md5绕过欸 A Dark Room upload Aura 酱的礼物 HTTP 是什么呀 url转义&#xff1a; 是将URL中的特殊字符转换为有效的ASCII字符格式的过程&#xff0c;以确保URL的正确解析和传输。这个过程涉及到将非ASCII字符替换为“%h…

【论文阅读】通用的语义-几何表征的机器人操作

文章目录 1. 【2023CoRL】A Universal Semantic-Geometric Representation for Robotic Manipulation针对痛点和贡献引言模型框架思考不足之处 2. Leveraging Locality to Boost Sample Efficiency in Robotic Manipulation摘要和结论引言模型框架实验思考不足之处 1. 【2023Co…

哈希表的查找、插入及删除——217、633、349、128、202、500,290、532、205(五简四中)

217. 存在重复元素&#xff08;简单&#xff09; 给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 &#xff0c;返回 true &#xff1b;如果数组中每个元素互不相同&#xff0c;返回 false 。 解法一、哈希 无则加入&#xff0c;有则代表重复&#xff0c;返回true …

Vue自定义横向轮播图

目录 前言代码效果演示详细代码实现思路轮播图实现代码组件使用代码前言 汇总一个最近写出来的效果,最新的设计稿里面要求实现一个轮播图,原本使用的Element-UI提供的轮播图不是很适配,所以选择自定义一个使用。文中附带代码实际效果演示视频。 大致需求就是:一行10个d…

敲桌子游戏

题目 在饭局上&#xff0c;会玩一种敲桌子的游戏&#xff0c;从1报数到100&#xff0c;如果报到个位含有7&#xff0c;或者十位含有7&#xff0c;或者7的倍数的数字&#xff0c;则报数人不能报出该数字&#xff0c;而是敲桌子代替。如果违反了规则&#xff0c;则进行一定的惩罚…

【python】逐步回归(多元线性回归模型中的应用)

文章目录 前言一、逐步回归1. 前进法&#xff08;Forward Selection&#xff09;2. 后退法&#xff08;Backward Elimination&#xff09;3. 逐步回归法&#xff08;Stepwise Regression&#xff09; 二、示例三、代码实现----python 前言 Matlab中逐步回归的实现可以使用 Mat…

流量分析-Windows

目录 介绍步骤 介绍 1、Medusa工具是通过并行登陆暴力破解的方法&#xff0c;尝试获取远程验证服务访问权限&#xff0c;它支持AFP, CVS, FTP, HTTP, IMAP, MS-SQL, MySQL, NCP (NetWare),NNTP, PcAnywhere, POP3, PostgreSQL, rexec, rlogin, rsh, SMB, SMTP(AUTH/VRFY), SNM…

【Windows下Oracle 11G 安装教程】

Windows下 Oracle 11G 安装及配置教程 引言数据库安装安装流程1.运行可执行程序2.取消安装更新配置3.安装及配置数据库4.选择系统的类别5.安装位置及全局数据库6.检查配置及设置情况7.配置对应数据库管理的密码 引言 在网上各种安装教程其实已经很多了&#xff0c;没必要再出这…

共享内存及网络通信

共享内存 ------ 最高效的进程间通信 一个内核预留的空间&#xff0c;两进程绑定同一块共享空间 避免了用户空间 到 内核空间的数据拷贝 IPC 操作流程 key值 > 申请 >读写 >关闭 >卸载 1,产生key值 函数ftok key_t ftok(const char *pathname, int proj_id);…

谷粒商城实战笔记-230-商城业务-认证服务-页面效果完成

这一节主要是完善各种页面效果。 一&#xff0c;用户名密码登录接口存放session 之前是在微博登录成功后把用户信息放入session&#xff0c;用户名密码登录成功也需要把用户信息放入session。 PostMapping(value "/login")public String login(UserLoginVo vo, R…

江协科技STM32学习- P5 GPIO输出

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…