yo!这里是Linux信号相关介绍

目录​​​​​​​

前言

基本介绍

概念

信号列表

信号处理

产生(发送)信号

通过按键产生

系统函数产生

软件条件产生

硬件异常产生

阻塞信号

信号状态

sigset_t

状态相关函数

1.sigprocmask

2.sigpending

捕捉信号

内核态与用户态

捕捉过程

sigaction

后记


前言

        先甭说linux信号,想一想生活当中存在哪些信号,有红绿灯、发令枪,手机提示音......,比如对于红绿灯而言,我们在过马路时候会看红绿灯并肉眼收到红绿灯信号,在看到红绿灯后我们也知道该做出什么动作,红灯停下绿灯直行。那么对于linux的信号,也是如此,os约定或规定了一些信号,在某时刻os发送给进程相应的信号,进程收到信号以后根据约定做出相应的动作,这就是关于信号的一个宏观的描述。

        在之前讲过地,当一个进程运行时,我们按下ctrl+c可以终止此进程,这里ctrl+c表示给进程发送2号信号,此信号地默认处理动作是终止,因此按下之后,进程就会立马终止掉,下面来看看关于信号产生和发送的具体细节吧。

基本介绍

  • 概念

        信号(signal)是一种进程间通信机制,是操作系统传递给进程的一种通知。它被用来通知进程发生了某种特殊情况,如外部事件的发生,如终端输入、中断、定时器到期等等。具体地,用户或os通过发送一定的信号通知进程,某些事件已经发生,你可以立即或者后续处理,因此信号产生与接收对于两个进程来说是异步的,进程必须将信号临时记下,方便后面处理。其中,异步表示两个进程互不等待,各自干各自的事,相反同步是一个进程发信号给另一个进程,之后这个进程等待另一进程的反馈后才继续运行,而异步则是不等待,发出信号后接着继续向后运行。

  • 信号列表

        如下图1,使用kill -l命令查看os定义的信号列表。其中1-31号信号属于普通信号,也是常见的一部分信号,32-64属于实时信号(不重点讨论)。其实看到这些信号都是大写的,应该知道是宏定义。当os规定这些信号时,也规定了默认的执行动作是什么,可通过man 7 signal命令查看,如下图2、3。

  • 信号处理

        对于上面的大部分函数,我们可以通过signal函数捕捉某信号来决定其处理方式,有三种方式可以选择:

        ①忽略,也就是不作为;

        ②默认,执行os规定的默认执行动作;

        ③自定义捕捉,用户提供一个处理函数,进程处理此信号时会执行此函数。

功能:修改(允许修改的)信号处理方式,

参数:signum传入信号编号,sighandler_t是函数指针,传入SIG_IGN表示捕捉动作为忽略,传入IGN_DFL表示捕捉动作为默认动作,传入你所提供的处理函数的函数名表示执行自定义行为,

返回值:若成功,返回值也是函数指针,是指向之前的信号处理函数的指针,若失败则返回SIG_ERR。

eg:

void func(int signnum)
{
    sleep(1);
    printf("\n%d号信号正在处理,pid:%d\n",signnum,getpid());
}

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

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

产生(发送)信号

        本质上,进程pcb内部具有保存信号的相关数据结构(位图),即信号位图字段(可以看到普通信号正好31个),通过修改0/1比特位来标识是否存在此信号,因此os向目标进程写信号——修改pcb中指定位图结构,完成信号发送,而此进程也是在合适的时候通过查看信号位图结构以进行相应动作。而信号的产生方式又分为以下几种,包括通过按键产生、系统函数产生、软件条件产生、硬件异常产生,下面分别介绍。

  • 通过按键产生

        按键产生信号,最常见的就是在前言中说过的ctrl+c发送2号信号SIGINT,默认处理动作是终止程序,此外还有3号信号SIGQUIT,按ctrl+\可以产生,默认处理动作是终止程序并且Core Dump

Core Dump解释:

        中文叫作核心转储,当一个进程异常终止时,用户可以选择允许产生core文件,把进程的用户内存数据结构全部保存到磁盘上,命名为【core.进程pid】。不止上面的3号信号可以core dump,很多信号都可以,具体可以通过man 7 signal查看信号手册。对于一个可以core dump的信号,默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,产生不安全的影响,但用户可以通过ulimit指令选择允许产生core文件,比如说允许core文件大小最大为1024k:ulimit -c 1024,而ulimit -c 0则是当用户不需要核心转储的时候不产生core文件,过程中可以通过ulimit -a来查看core文件大小等相关信息,如下图。

        如上就生成了core文件,使用gdb指令在调试过程中,输入【core-file core文件名】加载core文件即可来到产生信号的代码处进行调试,具体可看相关文章,这里不再赘述。

  • 系统函数产生

        之前讲过,我们可以通过指令【kill -信号编号 进程pid】给某进程发送信号,而kill指令是通过kill函数实现的,除此之外,还有raise函数、abort函数,这些都是os所提供的产生信号的系统函数,具体如下。

kill函数:给指定进程发送指定信号,

raise函数:给函数所在进程发送指定信号,

abort函数:给函数所在进程发送6号信号SIGABRT,直接终止。

        前两个函数都是成功返回0,失败返回-1,abort函数就像exit函数一样,总是成功执行,无返回值。

eg:

int main()
{
    int n=5;
    while(n--)
    {
        sleep(1);
        printf("%d\n",n);
    }
    
    //kill(getpid(),2);
    raise(2);

    n=5;
    while(n--)
    {
        sleep(1);
        printf("%d\n",n);
    }
    return 0;
}

  • 软件条件产生

        在之前的管道章节中说过,如果所有管道读端对应的文件描述符被关闭,则write操作会产生SIGPIPE信号,进而导致write进程退出,这里的SIGPIPE就是一种由软件条件产生的信号,此外如下的alarm函数可以产生SIGALRM信号,也是一种由软件条件产生的信号,alarm可以设定一个闹钟,在指定秒数后给当前进程发送SIGALRM信号,默认处理动作是终止当前进程。

        其中,seconds可以指定秒数,返回值是0或者是此闹钟之前所设定的闹钟还剩下的秒数。

eg:

void func(int signnum)
{
    sleep(1);
    printf("\n%d号信号正在处理,pid:%d\n",signnum,getpid());
}

int main()
{
    signal(14,func);

    alarm(10);
    int n=10;
    while(n--)
    {
        if(n==7)
        {
            int ret2=alarm(3);
            cout <<"ret2:"<<ret2<<endl;
        }
        sleep(1);
        cout<<n<<endl;
    }
    return 0;
}

  • 硬件异常产生

        硬件异常被硬件以某方式检测到并通知内核,然后内核像该进程发送适当的信号。比如说,

①执行到除0的代码,cpu的运算单位会发生异常,os就会发送SIGFPE信号给此进程,本质上cpu内部的状态寄存器有对应的状态标记位(溢出标记位),os在计算完毕后检测到溢出标记位是1,则为异常,之后发送信号给进程;

②进程访问非法内存地址,比如空指针、越界等,MMU(Memory Manage Unit,硬件,与页表一起进行内存映射)会产生异常,os此时发送SIGEGV信号给进程,本质上访问非法地址就是在将虚拟地址转物理地址的过程中,MMU一定会报错,之后os会给对应进程发送信号。

        值得注意的是,发生硬件异常,进程不一定会退出,只是默认处理动作里有退出操作,当我们自定义行为,不进行exit等相关退出操作时,就会死循环,因为硬件异常一直未被处理。

eg:

void func(int signnum)
{
    sleep(1);
    printf("\n%d号信号正在处理,pid:%d\n",signnum,getpid());
}

int main()
{
    signal(SIGFPE,func);
    int a=10;
    int b=0;
    int c=a/b;
    while(1)
       sleep(1);
    return 0;
}

阻塞信号

  • 信号状态

信号抵达(delivery):执行信号的处理动作

信号未决(pending):信号从产生到抵达的状态

阻塞(block):使信号保持在未决状态,直到解除才执行处理动作

        值得注意的是,阻塞和处理动作中的忽略不一样,信号被阻塞了就不会被抵达,而忽略是信号抵达了之后的一种处理动作。如下图是信号在内核中的相关数据结构示意图,在pcb中,存在block阻塞信号集、pending信号集、handlers处理方法表等相关数据结构。

block阻塞信号集:也叫信号屏蔽字,是一种位图结构,下标是信号编号,表中的1/0代表对应信号是否被阻塞

pending信号集:也是位图结构,下标是信号编号,表中的1/0代表对应信号是否是未决状态

handlers处理方法表:是一个函数指针数组,下标也是信号编号,当拿到信号编号signum时,并不是直接handlers[signum]()调用此函数,而是先判断(int)handers[signum]==0,则执行默认(SIG_DFL)动作,若==1(SIG_IGN)则执行忽略动作,若都不是然后才执行handlers[signum]()调用函数,其中SIG_DFL、SIG_IGN是由0、1强转成函数指针类型的宏定义,如下图:

        当os给某进程发送信号,也就是将此进程中的pending表中的对应位置由0置1,在合适的时候,进程会“遍历”pending表,遇到1后,不是直接调用handlers表中的函数,而是去block表中查看对应信号是否被阻塞,若block为1则不作为(等之后解除阻塞再说),若block为0则直接去调用对应函数。

  • sigset_t

        sigset_t是os提供的位图结构类型,与c++中的位图一样,但是我们不必究其实现细节,只需要记住关于它的操作函数,也就是由0置1、由1置0等相关接口,包括

#include <signal.h>
int sigemptyset(sigset_t *set);//将所有信号对应bit置0
int sigfillset(sigset_t *set);//将所有信号对应bit置1
int sigaddset (sigset_t *set, int signo);//将对应信号bit置1
int sigdelset(sigset_t *set, int signo);//将对应信号bit置0
int sigismember(const sigset_t *set, int signo);//判断对应信号bit是否为1

        上面前四个函数都是成功返回0,失败返回-1,后一个是bool函数,出错返回-1。

注意:使用sigset_t类型的变量之前一定要先使用sigemptyset、sigfillset接口初始化

 eg:

int main()
{
    sigset_t st;
    sigemptyset(&st);

    for(int i=31;i>=1;i--)//打印block表函数
    {
        if(sigismember(&st,i))
            cout<<"1";
        else
            cout<<"0";
    }
    cout<<endl;

    sigaddset(&st,1);
    sigaddset(&st,2);
    sigaddset(&st,3);

    sigdelset(&st,3);

    //sigfillset(&st);

    for(int i=31;i>=1;i--)//打印block表函数
    {
        if(sigismember(&st,i))
            cout<<"1";
        else
            cout<<"0";
    }
    cout<<endl;
    return 0;
}

  • 状态相关函数

1.sigprocmask

        sigprocmask函数可以修改或者读取当前进程的信号屏蔽字。如下,

set:一组将要添加或删除的信号集合

how:可以填SIG_BLOCK、SIG_UNBLOCK和SIG_SETMASK,SIG_BLOCK代表将set的信号增加到信号屏蔽字中,SIG_UNBLOCK代表将set中的信号从信号屏蔽字中删除,SIG_SETMASK表示直接将信号屏蔽字设置成set

oset:输出型参数,备份修改之前的信号屏蔽字(若不需要可置空),若未修改(即set为空),则该函数可得到信号屏蔽字

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 

        举个例子,比如说你想要阻塞1、2、3信号,就要通过sigset_t接口将set对应1、2、3比特位由0置1,再传入函数,此时how就得设置成SIG_BLOCK,同时若你想要原来的信号屏蔽字,则在外面定义一个set变量放入oset,执行函数之后,则此set就是原来的信号屏蔽字。

eg:

void handler(int signum)
{
    cout<<"signum:"<<signum<<","<<"pid:"<<getpid()<<endl;
}

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

    sigset_t set,oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set,2);
    sigaddset(&set,3);

    sigprocmask(SIG_BLOCK,&set,nullptr);//屏蔽2、3号

    int n=10;
    while(n--)
    {
        sleep(1);
        cout<<n<<endl;
    }
    sigdelset(&set,2);
    sigprocmask(SIG_UNBLOCK,&set,nullptr);//解除3号
    
    while(true)
        sleep(1);
    return 0;
}

2.sigpending

        sigpending函数可以读取当前进程的未决信号集,通过set这个输出型参数传出,成功返回0,失败返回-1。

#include <signal.h>
int sigpending(sigset_t *set); 

 eg:

int main()
{
    sigset_t pendingset;
    sigemptyset(&pendingset);

    sigset_t blockset;
    sigemptyset(&blockset);
    sigaddset(&blockset,2);
    sigaddset(&blockset,3);
    sigprocmask(SIG_BLOCK,&blockset,nullptr);//屏蔽2、3号

    while (1) // 1秒打印一次pending表
    {
        sleep(1);
        sigpending(&pendingset);

        for (int i = 31; i >= 1; i--)
        {
            if (sigismember(&pendingset, i))
                cout << "1";
            else
                cout << "0";
        }
        cout << endl;
    }
    return 0;
}

         综上,我们可以去block一个信号,也可以去捕捉一个信号,那当我们把所有信号捕捉或者堵塞,那么是不是就写了个不会被异常或用户杀死掉的进程了?当然不是,os也想到这一点了,其中有一个信号9号信号(SIGKILL)不可被阻塞或者捕捉,可使用此信号终止任何进程。

捕捉信号

  • 内核态与用户态

        前面提到地,进程接受到信号,可能无法立即处理,需要在合适的时候处理,那什么时候合适呢?答:从内核态返回用户态的时候,进行信号检测和处理。

内核态:os执行自己代码的状态,此状态具有很高的优先级

用户态:执行用户写的代码时的状态,是一个受管控的状态

注意:

        ①内核态返回用户态之前因为什么进入内核态?因为需要进行系统调用、缺陷、陷阱及异常等

        ②cpu内有状态寄存器CR3来标识这两种状态

        我们知道,如下图,进程地址空间有4G,其中3G是用户地址空间,1G是内核地址空间,用户地址空间通过用户级页表映射到物理内存中,内核地址空间通过内核级页表(可以被所有进程看到)映射到物理内存上,顾名思义,用户写的代码“占用”的是用户地址空间,而内核地址空间“存储”的是os自带的系统调用代码等,内核地址空间在每个进程地址空间都有一份(可类比于动态库的调用模式)。因此在用户地址空间运行叫做用户态,在内核地址空间运行叫做内核态。

  • 捕捉过程

        如果信号的处理动作是用户自定义函数,在信号递达时调用这个函数,这称为捕捉信号如下图是信号捕捉的流程,可以看出,这像一个无穷大符号,以此来简化记忆。

注意:

  • 每个箭头穿过用户地址空间和内核地址空间的分界线的地方就是用户态和内核态的一次转化
  • 对于③,这一步就是上面说的先检查pending表,再看block表,然后去handlers表调用函数的过程,只不过这里是捕捉信号的过程,没有谈及处理动作是忽略或默认的情况,若处理动作是忽略,则将pending表对应比特位由1置0,再返回主函数中向下执行,若处理动作是忽略,则处理动作是默认,一般是终止,则停止调度此进程,将pcb、地址空间等释放,并且不用再返回主函数处
  • 对于④->⑤,想一下为什么执行完了自定义处理函数之后不直接返回到主函数的中断处,还要先回到内核地址空间再返回?因为一方面执行完handler函数后需回到内核将pending表对应位置由1置0,另一方面只有内核知道当初从主函数中断的地方,回到内核才能拿到关于此的上下文。

  • sigaction

        sigaction函数可以读取和修改与指定信号相关联的处理动作。

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 

返回值:调用成功则返回0,出错则返回-1。

参数:

        signo:信号编号。

        act:若act指针非空,则根据act修改该信号的处理动作;

        oact:若oact指针非空,则通过oact传出该信号原来的处理动作。

其中struct sigaction是结构体,具体如下:

        其中,sa_handler变量传入自定义处理函数,sa_mask是sigset_t类型的变量,可以传入一个信号集,表示若想要调用此函数时自动屏蔽一些信号,可以把想要屏蔽的信号放进此变量中,其他的成员使用与实时信号有关,暂时不考虑,其中sa_flags可以设置为0。

eg:

void handler(int signum)
{
    cout << "signum:" << signum << ","
         << "pid:" << getpid() << endl;
}

int main()
{ 
     struct sigaction act;
     act.sa_handler=handler;
     sigaction(2,&act,nullptr);

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

        捕捉信号不仅可以使用这里介绍sigaction函数,也可以使用前面介绍的signal函数,看以看出signal函数的使用比sigaction函数简单一些,但sigaction函数的功能比signal函数要多一些,下面看一下这两个函数的区别

①signal函数每次设置的信号处理函数只能生效一次,在执行完此处理函数后,随即将信号处理函数恢复为默认处理方式。所以如果想多次相同方式处理某信号,就得在处理函数中再次调用signal设置,如下。但是sigaction函数设置后并且执行了处理函数后,一直有效不会重置

②sigaction函数保证了在信号处理函数被调用时,系统建立的新信号屏蔽字会自动包括当前正在递达的信号。因此在处理一个给定信号时,如果这种信号再次发生,那么它会被阻塞到处理结束为止,同时sigaction结构体的sa_mask可以保证另外的你想要屏蔽的信号。

int handler(int signum)
{
    //...
    signal(SIGINT, handler);
    //...
}
int main()
{
    signal(SIGINT, handler);
    //...
}

后记

        信号这一章节的知识点不算特别难,但是算比较多的,还涉及到与线程部分耦合的知识点还未讲解,将会在后面的多线程章节进一步阐述,理解信号这一领域的知识点 不能与现实情境脱离,借助现实情况可简化理解难度,比如红绿灯发出的信号、信号枪发出的信号。在面试中,信号也是一个高频的考点,包括信号产生的过程,捕捉信号的过程,及涉及到其中的系统函数,都在本文章中一一介绍过,多看几遍,拜拜!


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

分库分表及ShardingShpere-proxy数据分片

为什么需要分库&#xff1f; 随着数据量的急速上升&#xff0c;单个数据库可能会QPS过高导致读写耗时过长而出现性能瓶颈&#xff0c;所以需要考虑拆分数据库&#xff0c;将数据库分布在不同实例上提升数据库可用性。主要的原因有如下&#xff1a; 磁盘存储。业务量剧增&…

nodejs项目设置全局变量(global)

文章目录 前言一、使用global二、解决type typeof globalThis has no index signature.ts问题1、新建 /types/global.d.ts文件2、或者直接在入口文件/src/index.ts定义 三、最终效果鼠标放在global上&#xff0c;可显示global的类型生效了~ ![在这里插入图片描述](https://img-…

I.MX RT1170双核学习(2):双核相互激活和启动流程

RT1170这个芯片带有双核&#xff1a;Cortex-M7和Corterx-M4&#xff0c;两个核都可以独立地运行&#xff0c;当然双核也可以同时运行。在上一篇文章中&#xff0c;介绍了一下在RT1170中消息模块MU的使用&#xff1a;双核通信之MU消息单元详解&#xff0c;因为这是双核之间用来通…

05 python数据容器

5.1 数据容器认识 5.2 python列表 5.2.1 列表的定义 演示数据容器之&#xff1a;list 语法&#xff1a;[元素&#xff0c;元素&#xff0c;....] #定义一个列表List List [itheima,uityu,gsdfg] List1 [itheima,6666,True] print(List) print(List1) print(type(List)) pr…

smartKettle离线部署及问题记录

目录 &#x1f4da;第一章 前言&#x1f4d7;背景&#x1f4d7;目的&#x1f4d7;总体方向 &#x1f4da;第二章 部署&#x1f4d7;源码下载&#x1f4d7;后端部署&#x1f4d5;导入后端项目&#x1f4d5;修改settings.xml(自动下载相关jar包)&#x1f4d5; 编译&#x1f4d5; …

0x13 链表与邻接表

0x13 链表与邻接表 数组是一种支持随机访问&#xff0c;但不支持在任意位置插入和删除元素的数据结构。与之相对应&#xff0c;链表支持在任意位置插入或删除元素&#xff0c;但只能按顺序依次访问其中元素。我们可以使用一个struct来表示链表的节点&#xff0c;其中可以存储任…

MySQL线上死锁案例分析

项目场景 项目开发中有两张表&#xff1a;c_bill(账单表)&#xff0c;c_bill_detail(账单明细表)&#xff0c;他们的表结构如下&#xff08;这里只保留必要信息&#xff09;&#xff1a; CREATE TABLE c_bill_detail (id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主…

Gin之GORM 查询语句

前期工作可以看之前的&#xff08;连接数据库&#xff1b;以及确定要操作的库&#xff09; Gin之GORM 操作数据库&#xff08;MySQL&#xff09;-CSDN博客https://blog.csdn.net/m0_72264240/article/details/134948202?spm1001.2014.3001.5502这次我们操作gin库下的另外一个…

Lenovo联想拯救者Legion Y9000X 2021款(82BD)原装出厂Windows10系统

链接&#xff1a;https://pan.baidu.com/s/1GRTR7CAAQJdnh4tHbhQaDQ?pwdl42u 提取码&#xff1a;l42u 联想原厂WIN10系统自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、联想电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘 文件格式&am…

记录汇川:套接字TCP通信-梯形图

H5U集成一路以太网接口。使用AutoShop可以通过以太网方便、快捷对H5U进行行监控、下载、上载以及调试等操作。同时也可以通过以太网与网络中的其他设备进行数据交互。H5U集成了Modbus-TCP协议&#xff0c;包括服务器与客户端。可轻松实现与支持Modbus-TCP的设备进行通讯与数据交…

Redis哨兵模式:什么是哨兵模式、哨兵模式的优缺点、哨兵模式的主观下线和客观下线、投票选举、Redis 哨兵模式搭建

文章目录 什么是哨兵模式哨兵模式的优缺点主观下线和客观下线投票选举哨兵模式场景应用Redis version 6.0.5 集群搭建下载文件环境安装解压编译配置文件启动关闭密码设置 什么是哨兵模式 哨兵模式是Redis的高可用解决方案之一&#xff0c;它旨在提供自动故障转移和故障检测的功…

数据分析基础之《numpy(3)—基本操作》

一、基本操作 1、adarray.方法() 2、np.函数名() 二、生成数组的方法 1、生成0和1的数组 为什么需要生成0和1的数组&#xff1f; 我们需要占用位置&#xff0c;或者生成一个空的数组 &#xff08;1&#xff09;ones(shape[, dtype, order]) 生成一组1 shape&#xff1a;形…

STM32读取EEPROM存储芯片AT24C512故障然后排坑记录

背景&#xff1a; 有一个项目用到STM32F091芯片去读取 AT24C512C-SSHD EEPROM 芯片&#xff0c;我直接移植了之前项目的IIC库&#xff0c;结果程序运行后&#xff0c;读不出EEPROM里面的数据。 摘要&#xff1a; 本文主要介绍一个基于STM32F091芯片和AT24C512C-SSHD EEPROM芯片…

Java面向对象思想以及原理以及内存图解

文章目录 什么是面向对象面向对象和面向过程区别创建一个对象用什么运算符?面向对象实现伪代码面向对象三大特征类和对象的关系。 基础案例代码实现实例化创建car对象时car引用的内存图对象调用方法过程 成员变量和局部变量作用范围在内存中的位置 关于对象的引用关系简介相关…

6、生产者压缩算法面面观

生产者压缩算法面面观 1、怎么压缩&#xff1f;2、何时压缩&#xff1f;2.1、生产者端2.2、Broker 端 3、何时解压缩&#xff1f;4、各种压缩算法对比 压缩的思想&#xff0c;实际就是用时间去换空间的经典 trade-off 思想&#xff0c;在 Kafka 中&#xff0c;就是用 CPU 时间去…

Linux | 多线程

前言 本文主要介绍多线程基础知识&#xff0c;以及使用多线程技术进行并发编程&#xff1b;最后会介绍生产者消费者模型&#xff1b; 一、线程基本认识 1、什么是线程 如果你是科班出生&#xff0c;你肯定听过线程相关概念&#xff1b;但是你可能没有真正搞懂什么是线程&#…

十八)Stable Diffusion使用教程:艺术二维码案例

今天说说怎么样使用SD生成艺术二维码。 我们直接上图。 方式有三种,分别如下: 1)方式一:直接 contronet 的tile模型进行控制 使用QRBTF Classic生成你的二维码。 首先输入网址,选择喜欢的二维码样式(推荐第一种就行): 然后选择相应参数,这里推荐最大的容错率,定…

Linux 安装图形界面 “startx”

———————————————— 报错&#xff0c;如下&#xff1a; bash :startx command not found ———————————————— 解决方法&#xff1a; 1.先安装 — X Windows System&#xff0c;输入以下命令&#xff1a; yum groupinstall “X Window System”…

第一个“hello Android”程序

1、首先安装Android studio&#xff08;跳过&#xff09; Android Studio是由Google推出的官方集成开发环境&#xff08;IDE&#xff09;&#xff0c;专门用于Android应用程序的开发。它是基于JetBrains的IntelliJ IDEA IDE构建的&#xff0c;提供了丰富的功能和工具&#xff0…

2002-2023年各省环境规制力度数据(ZF报告词频环境规制关键词词频统计)

2002-2023年各省环境规制力度数据&#xff08;ZF报告词频环境规制关键词词频统计&#xff09; 1、时间&#xff1a;2001-2022年 2、指标&#xff1a;文本总长度、仅中英文-文本总长度、文本总词频-全模式、文本总词频-精确模式、环境规制力度词频和、环境保护、环保、污染、能…
最新文章