【Linux学习】进程控制

news2025/2/25 3:35:59

🐱作者:一只大喵咪1201
🐱专栏:《Linux学习》
🔥格言:你只管努力,剩下的交给时间!
请添加图片描述

在前面,我们学习了进程的相关概念,在这里本喵会给大家介绍如何控制进程。

进程控制

  • 一、进程创建
    • 如何理解fork有两个返回值?
    • 如何理解fork之后,给父进程返回子进程的pid,给子进程返回0?
      • 写时拷贝
    • 如何理解,父子进程让if和else if同时执行?
  • 二、进程终止
    • 程序执行完毕
      • exit()和_exit()的区别
    • 程序异常终止
  • 三、进程等待
      • 回收子进程资源
      • 获取退出信息
      • 阻塞和非阻塞等待
  • 四、进程程序替换
    • 程序替换原理
    • 替换函数
    • 替换不同类型的程序
  • 五、shell的简单实现
    • 内建/内置命令
  • 总结

一、进程创建

  • 函数:pid_t fork(void);
  • 返回值:
    -1:子进程没有创建成功。
    0:该值传给子进程。
    pid值:该值传给父进程。
  • 作用:创建一个子进程,如果创建成功,返回两个值。

之前我们都是直接在用这个函数,它的原理到底是什么样我们并不清楚,现在本喵来回答三个问题。

如何理解fork有两个返回值?

这个问题是不是一直都在困扰着你?一个函数居然有两个返回值,这是怎么做到的呢?在学习了进程地址空间以后就可以回答这个问题了。

在一段代码中,执行完fork函数以后就会创建出子进程,此时就同时存在两个进程。

fork函数是一个系统调用,但是它仍然是一个函数,只是执行者是操作系统而已,既然有返回值,所以该函数的最后一条语句肯定是return。

图
fork函数中做的事情大概如上图,这里仅是一个感性的介绍,可以看到,在return之前,该函数的核心逻辑已经做完了,就和我们平时写的函数一样,return的时候,核心逻辑肯定是执行完的。

换句话说,在执行return的时候,子进程已经被创建成功了。

此时父进程和子进程都会执行return语句,都会返回pid值,所以表现出来的就是fork有两个返回值。

如何理解fork之后,给父进程返回子进程的pid,给子进程返回0?

现在已经知道,父子进程都会执行return语句,所以会有两个返回的pid值,但又是怎么做到给父进程和子进程返回的值是不同的呢?

在这里本喵正式介绍一下写时拷贝。

写时拷贝

图
上图描述了子进程创建的过程。

在父进程创建子进程之前,它有自己的PCB,还有自己的进程地址空间,并且通过页表的映射,在物理内存中也有相应的内存空间。

当执行完fork以后,子进程诞生了,它同样具有自己的PCB以及进程地址空间和页表。

但是此时,子进程的数据段和代码段等内容经过页表的映射,也指向了父进程在物理内存中的空间。

  • 子进程会继承父进程的资源。

也就是说,子进程在刚创建的时候,它和父进程共用同一块物理内存。

图
当子进程尝试这对数据段中的内容做修改的时候,就会发生写时拷贝。

所谓写时拷贝,就是在物理内存中,将被修改空间中的内容先拷贝到另一个物理空间中,并且修改对应的页表映射关系,再修改拷贝后新空间中的内容。

pid_t id这个用来接受返回值的变量,在子进程刚创建的时候,在物理内存中是只有一个的,但是在返回的时候,父子进程return的值不一样,也就是物理内存中的id值被修改了,为了保证进程的独立性,所以就会发生写时拷贝。

  • 父进程和子进程谁先返回谁就发生写时拷贝,并且使用新物理空间,至于它俩是谁先返回的,这个是不一定的。

正是由于写时拷贝的存在,所以父进程和子进程能够接收到不同的返回值。

如何理解,父子进程让if和else if同时执行?

根据上面的介绍,我们知道,由于发生了写时拷贝,所以父进程和子进程都有一个id值,并且是不同的。

图
fork之后的代码,父子进程是共享的。

也就是说,在fork之后的代码,父子进程是共同执行的,并且父子进程使用的是同一块物理空间中的代码。

但是各自的id值是不同的,所以会父子进程会进入不同的条件判断中,并且执行不同的代码。

二、进程终止

进程终止指的就是程序执行结束了,进程终止退出的场景有三种:

  • 代码运行完毕且结果正确
  • 代码运行完毕但结果不正确
  • 代码异常终止

程序执行完毕

我们在写代码的时候,main函数最后总会有一个return 0,其中0就是进程终止的退出码。

退出码:用来标识程序的运行结果。

在C语言的库中,官方提供了很多的退出码:

图

图
将官方提供的退出码全部打印出来,可以看到,一共有134个退出码,其中退出码为0表示成功,退出码是其他的时候,都代表着不同的意义。

  • 0只有一个,所以用它来标识成功,其他数字有无数个,所以用非0数字表述程序执行的其他情况。

我们可以根据程序的退出码来判断程序执行的情况,在shell中也有专门的变量来查看程序的退出码:

图
写一个打印hello world的程序,return的值是0,也就是程序的退出码是0。在shell中使用本地变量?来查看退出码。

  • ?:这是一个本地变量,用了存放最近一个进程的退出码

上面的程序中,退出码都是0,标识的是成功,同样我们可以根据不同的情况返回不同的退出码。

图
上面程序是从1加到100,如果结果正确返回0,结果不正确返回1.

图
可以看到,退出码是1,表示结果不正确,此时我们是自己定义的退出码是1标识结果不正确。并没有使用官方库中提供的退出码。

return 后面的数字是进程的退出码,用echo查看的退出码是main函数中的return值,为什么不是加法函数中的return值呢?

只有main函数中的return值才能作为进程的退出码,其他自己实现的函数中的return值不能够作为退出码。

如果就我们就想在子函数中退出,并且有相应的退出码呢?

图

在加法函数中,如果结果正确,退出码是0,如果结果不正确,退出码是10.

图
可以看到,此时的退出码是10。

  • exit(int num)函数的作用是终止进程,并且进程的退出码就是它的参数。

exit()和_exit()的区别

图
此时使用_exit函数来终止进程。

图
可以看到,退出码仍然是10,和使用exit()的结果是一样的。

那么exit和_exit到底有什么区别呢?

图
使用exit()来终止进程。
图
可以看到,字符串正常打印了出来。

图
使用_exit()来终止进程。
图
运行结果中,并没有字符串的打印,这是什么原因呢?

图

  • 程序是属于用户层的,exit和_exit等函数都是在程序中的。
  • 调用exit()之后,还会再执行一些程序,比如刷新缓存区等操作,然后再调用_exit()终止进程,exit的_exit的封装。
  • 调用_exit()之后,会直接终止进程,不会执行刷新缓存区等操作。

区别:也就是说,_exit()是系统调用,而exit()是库函数。

所以在使用_exit的时候,缓冲区还没有来得及刷新,进程就被终止了,所以字符串就没被打印出来。

结论:程序执行完毕的情况下,只有main函数中的return和exit或者_exit的才能终止进程,并且返回退出码。

程序异常终止

异常终止就是程序执行到一半不再执行了,因为发生了异常。

图
上图中的程序,发生了除0操作,所以肯定是异常,如果不异常的话,会打印一个语句再终止进程。

图
可以看到,打印的语句并不是DIV执行成功,而是打印了异常信息,告诉我们浮点数除0了。

此时程序就没有执行完毕,但是异常终止了。

三、进程等待

图
进程等待主要使用上面的俩个系统调用,等待成功返回被等待进程的pid值。

进程等待的作用:

  • 回收子进程资源
  • 获取子进程退出信息

回收子进程资源

先来看看现象:
图
子进程在执行了10秒钟之后终止了,退出码是10.

图
此时子进程成了僵尸进程,如上图中红色框中所示的Z+。

图
此时就是连kill -9 都无法杀死这个僵尸进程,因为已经挂了的进程是无法再杀死的,所以它仍然是僵尸进程。

图
在子进程终止的10秒钟后,使用waitpid函数来等待子进程。

图
在子进程终止后的10秒钟内,子进程的状态是僵尸状态。

图
在进程等待成功以后,处于僵尸状态的子进程(pid值为26718)的进程就没有了,如上图中绿色框所示,此时子进程的资源也被回收了。

进程等待可以回收子进程资源。

获取退出信息

再看进程等待的系统调用:

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

该函数的三个参数中,第二参数status是一个输出型参数,进程退出的信息就放在这个变量里。

图
这个变量是一个32为的int类型的变量,在存放进程退出状态的时候只用到了16位。

  • [15:8]:这8位放的是进程的退出码。
  • [6:0]:这7位放的是进程异常退出时的终止信号。
    *[7]:这一位放的是一个标志位,以后本喵再详细介绍这一位。

图
通过移位操作,获取到status的次低8位和低7位,也就是进程退出码和终止信号信息并且打印出来。

图
可以看到,在子进程终止以后,父进程进行等待,等待成功以后打印出了子进程的退出码和终止信息。

  • 我们在子进程中使用exit(10)来终止进程。

图
此时我们是使用kill -9来终止的子进程,所以打印出来的终止进行就是kill -9。进程异常终止的情况下,程序的退出码就没有任何意义了。

图
不仅可以使用kill -9来终止进程,还可以使用其他信号来终止进程。

图
此时是使用kill -4终止的进程,所以终止信号就是4.

一个进程退出以后会变成僵尸,并且会把自己的退出信息写入自己的PCB中的task_struct中,如下图所示的变量中。
图
wait和waitpid是系统调用,所以操作系统是有资格去读取子进程的task_struct的,读取到以后会放在status变量中。

阻塞和非阻塞等待

waitpid的第三个参数就是用了决定进程等待的方式的。

我们上面演示的就是阻塞式等待,此时第三个参数是0。所谓阻塞等待就是父进程什么也不干,就在等子进程终止,终止以后父进程继续执行,来回收子进程资源并且获取它的退出信息。

非阻塞等待是和阻塞等待相反的,在等待子进程终止期间,父进程还可以干它自己的事情,此时第三个参数是WNOHANG。

  • waitpid的返回值:
    -1:等待失败,例如给它传入一个不存在的pid值,就会等待失败
    pid_t:等待成功以后,会返回被等待进程的pid值
    0:在非阻塞等待中,如果子进程没有终止,那么返回0,如果终止且等待成功,返回子进程的pid值。

图
将等待方式设定为非阻塞等待。
图
可以看到,虽然使用了进程等待,但是子进程和父进在同时执行。

这里有一个问题,是进行了非阻塞等待,但是子进程终止了,父进程也没有进行资源回收和退出信息获取啊,也就是对子进程只询问了一次,发现子进程没有结束父进程就干自己的事了,之后也没有再询问。

这样的逻辑执行一次就被叫做非阻塞等待。

而将这部分代码放在一个while循环中,就会进行多次非阻塞等待,也会对子进程进行多次询问,此时叫做轮询。

图
图
此时在子进程执行的过程中,父进程在干自己的事之前都会等待一次子进程,当子进程终止以后,父进程在干完自己手头的事情发现子进程终止了,然后获取到了子进程的退出信息。

非阻塞等待的好处就是不会占用父进程的所有精力,父进程可以在轮询期间干自己的事情。

四、进程程序替换

首先我们要知道,创建子进程的目的是什么?无非就是两种目的:

  • 想让子进程执行父进程代码的一部分。
  • 想让子进程执行一个全新的代码。

我们之前所写的程序,子进程都是在执行父进程代码的一部分,而要想让子进程执行全新的代码,就需要进行进程程序替换。

图
这是程序替换用到的系统调用,本喵会挨个给大家介绍。

先来看看进程程序替换是什么:
图
在子进程中,用ls -al来替换程序,并且执行。

图
此时,使用我们自己的程序同样可以实现ls -al的功能,因为子进程执行的就是ls -a -l程序。因为程序替换成功了,所以返回ls程序的退出码,如果替换失败,就会执行exit(1)。

程序替换原理

图
在子进程刚创建的时候,子进程和父进程通过页表映射到物理内存中空间是同一块空间,父子进程的代码段,数据段,堆,栈等区域都同一个。

图
当子进程中执行exec*()函数的时候,会发生写时拷贝,将原本物理内存中的数据段和代码段拷贝一份,放在新的物理内存中。

将磁盘中要替换的可执行程序覆盖到新的物理内存中,并且改变子进程原本的页表映射关系。

仅程序发生了替换(数据段和代码段),子进程的PCB中的task_struct仍然不变。

而且写时拷贝不仅在数据段发生,在代码段也可以发生,写时拷贝的目的同样是为了保证进程的独立性。程序替换之后,子进程执行的代码也不再是原本父进程中的代码,而是全新的代码,比如上诉例子中的ls程序。

替换函数

  1. int execl(const char* path, const char* arg,…);
  • 第一个参数是要替换程序所在路径,比如/usr/bin/ls,就是可执行程序ls所在的路径。
  • 除了第一个参数以外,后面的参数是可变参数,也就是参数的个数是可以变的。
  • 返回值:如果替换失败了就返回-1,替换成功了什么都不返回,因为程序已经被替换了,有没有返回值也没有意义。

可变参数我们在C语言中见过很多,比如printf:
图
这里的可变参数其实就是main函数的命令行参数中的char* argv,argv[0]是程序名,argv[1]及后面的是程序的选项,最后要有NULL结尾。

上面演示就是使用的execl()函数,本喵就不再演示了。

  1. int execlp(const char* file,const char* arg,…);
  • 第一个参数是要替换程序的程序名字,操作系统会自动去环境变量PATH中寻找该程序名。
  • 后面的参数和返回值是和execl一样的。

图
使用execlp的时候,只写了要替换的程序名ls,后面的可变参数也只写了ls。

图

由于替换ls的时候,没有加任何选项,所以现实的内容如上图中的红色框所示。

  1. int execle(const char* path,const char* arg,…,char* const envp[]);
  • 第一个参数和execl一样,也是要替换程序的路径。
  • 第二个开始直到倒数第一个之前都是可变参数,和execl还有execlp一样。
  • 最后一个参数是环境变量。

替换的程序也可以是我们自己写的程序。

图
自己写一个获取环境变量的程序,并且打印出来。

  • 自定义环境变量
    图
    原本,子进程和父进程都是使用的系统的环境变量,但是在子进程进行程序替换的时候,使用execle将自定义的环境变量传了过去,此时子进程在进行程序替换的同时,将原本系统的环境变量也进行了替换,替换成了自定义的环境变量。

图
可以看到,子进程打印出来的环境变量只有自己定义的3个环境变量。

  • 系统环境变量

如果想将自己定义的环境变量也加到系统环境变量中呢?
图
使用putenv()系统调用将自定义的环境变量增加到系统的环境变量中,在给execle传参的时候,第三个参数传environ。
在这图里插入图片描述
可以看到,子进程打印出来的环境变量,不仅有系统的,还有我们自己定义的。

对于系统的环境变量,即使不传参,子进程也是能获取到的,因为子进程会继承父进程的一切。

  1. int execv(const char* path, char* const argv[]);
  • 第一个参数同样是需要替换程序的路径,和execl一样。
  • 第二个参数不再是可变参数,而是将原本的可变参数放在一个指针数组中。

第二个参数的指针数组,和mian命令函数中的char* argv[]一样,argv[0]是程序名,argv[1]等之后的是选项,最后一个是NULL。
图
将ls -a -l 并且带颜色高亮的程序加选项以字符串的形式放在指针数组中,传给execv()函数。
图
结果和execl()一样。

  1. int execvp(const char* file, char* const argv[]);
  • 第一个参数是需要替换的程序名,同样不需要路径,系统回去环境变量PATH中寻找,和execlp一样。
  • 第二个参数和execv一样,不再讲解。

图
图

  1. int execvpe(const char* file, char* const argv[], char* const envp[]);

参数本喵都已经讲解过了,这里无非就是做了一些组合。

图
图
7. int execve(const char* path, char* const argv[], char* const envp[]);

图

参数类型本喵就不在这里继续讲解了,相信大家可以自己进行组合。

  • 该函数是在手册2中的,其他6个程序替换函数是在手册3中的。

图
图
前面的六个程序替换函数,都是execve()函数的封装。

封装了六个不同函数的目的就是方便我们用户根据不同的使用情况去调用。

替换不同类型的程序

使用上诉7个程序替换函数,并不是父进程是C程序,替换的程序也必须是C程序,而是任何类型的程序都可以,因为任何类型的程序最后都是二进制机器码。

图
在while循环中,将cpp,python,shell三种不同类型的可执行程序,分别替换一次并且执行。

图
可以看到,这三种类型的可执行程序都可以去替换。

进程程序替换可以替换任何类型的可执行程序。

五、shell的简单实现

现在我们就可以更加清楚shell的运行机制了,bash是一个父进程,每输入一个指令就会创建一个子进程,并且进行相应的程序替换。

现在我们就来简单的模拟实现一下:
图
使用fgets函数获取输入的一行字符串,并且将这一行字符串进行切割,比如“ls -a -l”切割为“ls”,“-a”,“-l”,并且将切割后的字符串放入myargv指针数组中。

图
通过打印可以看到,分割成功了。

图
字符串分割成功以后,就要创建子进程,在子进程中进行进程程序替换,因为替换的程序都是指令池中的,所以使用execvp()即可。

为了能够让我们自己的shell一直运行,这部分逻辑全部放到while(1)循环中。

图
在我们对shell运行起来以后,输入ls指令可以成功显示文件,但是没有发生文件类型的高亮。

图
在将字符串中的程序名分割出来后,判断一下程序名是不是ls,如果是对话给它加高亮选项。

图
此时我们自己的shell中,使用ls指令以后,显示的文件名同样有颜色高亮。

图
但是又出现了问题,更换了路径,但是使用pwd后的现实的路径却没有发生改变。

内建/内置命令

首先需要明白,当前目录是什么?

图

  • /proc中放的当前内存中存在的进程,176178是我们的shell的pid值,查看myshell进程文件的情况。
  • 最重要的信息有两条:
    橘黄色线:cwd就是表示当前路径,该路径就是当前进程的可执行程序所在的目录。
    红色线:当前进程的路径。

所以说,当前路径是相对于进程来说的,不同进程对应的当前路径也是不同的。

我们的shell中,使用cd改变了路径,但是使用pwd以后,现实的路径没有发生改变,原因是:

  • cd是子进程替换的程序,所以子进程执行的时候,确实改变了路径,但是当它终止后,它就退出了,所对应的cwd(当前路径也没有了)。
  • 再使用pwd指令,查看的是myshell的cwd(当前路径),而myshell的当前路径是不会发生改变的。

找到原因再找办法解决,有一个系统调用chdir可以改变当前路径:
图
更改成功返回0,更改失败返回-1。

图
在将字符串分割完以后,在创建子进程之前,需要判断一下是否是cd指令,如果是cd指令的话,需要更改当前目录,这个操作父进程就可以完成,不需要创建子进程。

图
此时更换路径就不再存在问题了。

同样的,echo也是一个内建命令,只需要父进程就可以完成,不需要创建子进程。

图
在我们对shell程序中,使用echo打印退出码时,结果并不是预期那样。

图
当指令echo的时候,单独处理打印的内容,并且不需要创建子进程,父进程就可以完成。

图
出来内建指令,其他子进程终止后,父进程获取它们的退出信息。

图
此时echo指令就可以正常使用了。

内建/内置指令的本质就是不用创建子进程去完成任务,父进程就足够了。

myshell.c的全部代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM]; //指针数组
int  lastCode = 0;
int  lastSig = 0;

int main()
{
    while(1)
    {
        // 输出提示符
        printf("[用户名@主机名 当前路径]# ");
        fflush(stdout);

        // 获取用户输入, 输入的时候,输入\n
        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        (void)s;
        // 清除最后一个\n , abcd\n
        lineCommand[strlen(lineCommand)-1] = 0; // ?
        //printf("test : %s\n", lineCommand);
        
        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
        // 字符串切割
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
        {
            myargv[i++] = (char*)"--color=auto";
        }

        // 如果没有子串了,strtok->NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));

        // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
        // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
        if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if(myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }
        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("%d, %d\n", lastCode, lastSig);
            }
            else
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }
        // 测试是否成功, 条件编译
#ifdef DEBUG
        for(int i = 0 ; myargv[i]; i++)
        {
            printf("myargv[%d]: %s\n", i, myargv[i]);
        }
#endif
        // 内建命令 --> echo

        // 执行命令
        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {
            execvp(myargv[0], myargv);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;
        lastCode = ((status>>8) & 0xFF);
        lastSig = (status & 0x7F);
    }
}

代码如上,全部都是本喵讲解到的知识点,本喵不再做详细解释。

图
可以看到,我们自己实现的shell,同样可以实现系统shell的部分功能,比如ls,cd,pwd等指令,因为在子进程中都是使用的指令池中的程序来替换的。

总结

进程控制内容是对前面进程学习内容的一种检验,很重要,尤其是进程程序替换,此时我们心中曾经的疑惑能够解开不少。希望对各位有所帮助。

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

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

相关文章

私人影院微信小程序源码,采用腾讯小程序云开发,包括影院动态,最新影讯,房间预约,后台预约管理,导出预约表格Excel数据等功能

功能介绍 私人影院是很多年轻人比较喜欢的地方&#xff0c;虽然空间小&#xff0c;但是他们喜欢在这样的空间里享受休闲的时光&#xff0c;通过提前预约订位&#xff0c;可以使商家和消费者节省时间&#xff0c;减少人力成本&#xff0c;大大方便业务的拓展&#xff0c;同时也…

Word控件Spire.Doc 【Table】教程(4):如何在C#、VB.NET中设置Word表格样式

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

备战蓝桥杯数学基础:位运算理论

[TOC](目录)位运算概述位运算就是基于整数的二进制表示进行的运算&#xff0c;由于计算机内部就是以二进制来村塾数据&#xff0c;位运算是相当快的。基本的位运算共6种&#xff0c;分别为按位与、按位或、按位异或、按位取反、左移和右移与、或、异或这三者都是两数间的运算&a…

【算法刷题】栈与队列题型及方法归纳

栈与队列的特点 1、栈&#xff1a;FIFO 栈是仅能对其一端进行操作的结构&#xff0c;实现后进先出的效果。在C中采用容器适配器的方式实现栈。&#xff08;容器适配器实际上就是对某一类型的对象进行泛化&#xff0c;定义了这一类泛化对象的可进行操作的逻辑&#xff09; 什么…

上半年要写的博客文章22

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…

极端尺度物体的显著性分割方法(SOD 新 SOTA)

Paper Link&#xff1a;http://cvteam.buaa.edu.cn/papers.htmlBackground&#xff1a;显著性物体分割在常规图像场景取得突破进展&#xff0c;在极端尺度物体场景仍面临挑战。图像前景物体分割是深度学习、计算机视觉等领域的研究热点&#xff0c;在机器视觉、智能交通、智慧医…

Mac 电脑磁盘空间释放记录

起因 点开钉钉页面就卡住&#xff0c;看了一下&#xff0c;光一个钉钉占到4G左右的内存&#xff0c;给钉钉发了工单&#xff0c;没人理我。又随手看了一下系统磁盘空间使用情况&#xff0c;发现快满了&#xff01;&#x1f613; 应用清单 我是一个应用的超轻度用户&#xff…

NFT市场聚合器:他们有没有得到广泛使用?他们对于冷门的NFT交易市场有什么影响?

聚合器是什么&#xff1f;NFT市场聚合器是一种交易工具平台&#xff0c;允许用户在一个页面同时间从多个不同的市场浏览和发现NFT。 这些聚合器将市场上的NFT挂单整合在一起&#xff0c;使用户更容易找到并购买NFT&#xff0c;并节省了用户在多个不同NFT市场之间比较来回比较NF…

rabbitMQ简介

rabbitMQ简介 目前应用最广泛的一个基于AMQP规范的开源的消息中间件RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现 rabbitMQ的重要概念 Message&#xff1a;消息&#xff0c;消息是没有名称的&#xff0c;消息由消息头和消息体组成&#xf…

腾讯安全联合Gartner发布SOC+白皮书,助力政企构筑实战化安全运营体系

随着数字化浪潮的蓬勃兴起&#xff0c;各类政企机构上云步伐加快。与此同时&#xff0c;如高危漏洞、勒索病毒、挖矿木马、APT攻击等威胁层出不穷&#xff0c;安全形势日益严峻。许多政企机构虽部署了较为完备的基础安全产品&#xff0c;但防御体系仍以异构设备堆叠式为主&…

STM32+ESP8266+机智云+DHT11数据上传

机智云 文章目录机智云前言一、工程的修改二、数据的上传1.标识符2.数据处理3.数据上传三、app控制前言 今天搞了一下机智云&#xff0c;就想把温湿度发到app上去&#xff0c;然后能够控制灯的开关。之前从来没有用过这个玩意&#xff0c;用阿里云和点灯科技多一点&#xff0c…

恭喜 OpenSergo 获得中国开源云联盟 2022 “优秀开源项目”

作者&#xff1a;OpenSergo 为了营造良好的开源氛围&#xff0c;为我国开源产业发展注入更多活力&#xff0c;中国开源云联盟(China Open Source Cloud League&#xff0c;简称“COSCL”) 组织开展了 2022 年度评选活动。并在今天的 2022 木兰峰会中公布了评选结果。 2022 木兰…

共享模型之管程(六)

1.park&unpark 1.1.概述 1>.他们是LockSupport类中的方法 // 暂停当前线程 LockSupport.park(); // 恢复某个线程的运行 LockSupport.unpark(暂停线程对象)注意:先park再unpark! 1.2.案例 Slf4j public class TestPark {public static void main(String[] args) th…

持续数据保护(CDP)适合档案数据备份吗?

几个月前笔者写过两篇针对档案长期保存库备份&#xff08;或者说是档案数字资源长期保存策略&#xff09;的文章&#xff0c;《电子档案备份相对于数据备份的特别之处》、《备份策略从“3-2-1”到“4-3-2-1”》&#xff0c;但是很多读者依然觉得不解渴&#xff0c;因为目前绝大…

Activity和Fragment的生命周期总结以及保存实例状态机制

读完将收获以下内容 一. Activity的生命周期详解 Activity配置和状态改变时生命周期 Activity状态和配置改变时的解决方案 二.Fragment的生命周期详解 Fragment配置和状态改变时生命周期 Fragmen状态和配置改变时的解决方案 三.Activity和Fragment的生命周期调用顺序 一. Acti…

笔记--Qt Pro语法总结

QT pro文件&#xff1a; 任何一个 Qt 项目都至少包含一个 pro 文件&#xff0c;此文件负责存储与当前项目有关的配置信息&#xff0c;比如&#xff1a; 项目中用到了哪些模块&#xff1f; 项目中包含哪些源文件&#xff0c;哪些头文件&#xff0c;它们的存储路径是什么&…

德育知识元素挖掘系统设计 软件工程 spring boot + Vue.js + python机器学习

第一章 绪论 摘要 当今社会发展迅速&#xff0c;机器学习相关技术快速在各行各业普及&#xff0c;制作数据挖掘系统的需求日益增长。同时&#xff0c;随着社会发展不断推进&#xff0c;对学生进行德育教育的规模越做越大&#xff0c;成为了教育行业发展的一大趋势。对于较大规模…

glibc 2.31 pwn——house of pig原题分析与示例程序

house of pig这种利用方式来源于XCTF 2021 final中的同名题&#xff0c;其原题使用的是libc 2.31版本&#xff0c;本文就根据这道题学习一下这种漏洞利用方式。 参考资料 这是一道C pwn&#xff0c;但漏洞本身与C不同于C的特性关系不大。 一共提供了5个选项&#xff1a; 增&…

idea无法安装插件

不能安装插件&#xff0c;首先需要确定是问题&#xff0c;最常见的是无法下载和安装不上&#xff0c; 1.无法下载解决版本 无法下载很多时候就是延迟太高导致的&#xff0c;我们先打开插件官网看一下 Python - IntelliJ IDEs Plugin | Marketplace 如果网站无法打开&#xf…

【测试】用例篇

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录一、设计测试用例的万能公式二、设计测试用例的具体方法1. 等价类2. 边界值3. 因果图&#xff08;判定表&#xff09;4. 场景设计法5. 正交法6. 错误猜测法:sparkles: 小结普通小孩也要热爱生活&#xff01; 一、设…