Linux系统编程:信号

目录

1.信号概念

2.信号产生

2.1 终端

2.2 系统调用

2.3 硬件异常

2.4 软件条件

2.5 小结

3. 进程退出时的核心转储问题

4. 信号捕捉初识

5. 阻塞信号

5.1 相关概念

5.2 在内核中的表示

6. 信号捕捉

6.1 知识铺垫

6.2 信号捕捉流程

6.3 sigset_t

6.4 信号集操作函数

6.5 sigaction

7. 可重入函数

8. 关键字volatile


1.信号概念

        信号是进程之间事件异步通知的一种方式,属于软中断。例:用户输入命令,在Shell下启动一个前台进程。用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。这里的ctrl+c就被OS解释成为了一种信号

注:
  • kill -l命令可以察看系统定义的信号列表
  • 当进程收到信号的时候,进程可能正在执行更重要的代码,信号不一定会被立即处理--因为信号可以随时产生(异步)
  • 进程本身必须要有对信号的保存能力
  • 进程在处理信号时,有三种动作--默认动作、自定义动作、忽略动作
kill -l

  每个信号都有一个编号和宏定义     --     [1, 31]:普通信号            [34, 64]:实时信号 

一个共识就是:信号是发送给信号的,进程是被保存到哪里了呢?-- task_strut中。当进程收到信号后,会修改PCB中的信号位图,unsigned int signal -- 其有32个比特位,比特位的位置代表编号,比特位的内容表示是否收到信号:0(未收到)、1(收到)

由上得出,信号的发送就是修改PCB中的位图结构,只有OS有权修改。本质就是OS向目标进程发送信号,所以会提供一系列的系统调用,使得用户可以通过OS发送信号。

2.信号产生

2.1 终端

        通过终端的按键产生信号,ctrl+c 和 ctrl+\ 默认动作都是终止进程,分别对应2和3号信号

2.2 系统调用

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int sig);

参数:pid:目标进程pid

           sig:发送几号信号

返回值:成功返回0,失败返回-1
可以向任意进程发送任意信号

 demo代码:

mysignal.cc/#include <iostream>    #include <signal.h>    #include <unistd.h>    #include <sys/types.h>    #include <string>    using namespace std;    static void Usage(const string& proc)    {    cout << "\nUsage:" << proc << " pid sino\n" << endl;    }    int main(int argc, char *argv[])    {    if(argc != 3)    {    Usage(argv[0]);    exit(1);    }    // 1.通过键盘发送信号    // 2.通过系统调用发送信号    pid_t pid = atoi(argv[1]);    int signo = atoi(argv[2]);    int n = kill(pid, signo);    if(n != 0)                                                                                            {    perror("kill");    }    return 0;
}///mytest.cc
#include <iostream>                                                                                          
#include <sys/types.h>    
#include <unistd.h>    using namespace std;    int main()    
{    while(1)    {    cout << "我是一个正在运行的进程,pid:" << getpid() << endl;    sleep(1);    }    return 0;    
}    

上面代码的目的是,先运行mytest进程,进程mysignal通过命令行参数找到mytest进程来终止该进程

int raise(int sig); -- 给自己发送任意信号 

  //mysignal.cc/  #include <iostream>#include <signal.h>#include <unistd.h>#include <sys/types.h>    #include <string>   int main(int argc, char *argv[])    {  int cnt = 0;    while(cnt <= 10)    {cout << "signal ss" << cnt++ << endl;if(cnt >= 5) raise(9);}return 0;
}

由上述现象可以看到,raise在循环五次后发送9号信号,杀死进程。  

#include <stdlib.h>

void abort(void); -- 使当前进程接收到信号而异常终止,abort函数总是会成功的 -- 6号信号

下面两个接口都可以通过kill接口实现 

对信号处理行为的理解:

  • 很多情况,进程收到大部分的信号默认处理动作都是终止进程
  • 信号的意义:信号的不同,代表不同的事件,但对事件发生之后的处理都做都可以一样。

2.3 硬件异常

        如:当前进程执行了除 以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。还有野指针问题 

野指针问题:

#include <stdio.h>
#include <signal.h>
void handler(int sig)
{printf("catch a sig : %d\n", sig);
}
int main()
{signal(SIGSEGV, handler);sleep(1);int *p = NULL;*p = 100;while(1);return 0;
}

若不进行信号捕捉怎么发生段错误

信号捕捉以后会捕捉到11号信号打印:

2.4 软件条件

2.4.1 管道

SIGPIPE是一种由软件条件产生的信号,管道中已经介绍过了。在管道中,读端关闭,写段一直写,会发生一场,OS向写端发送SIGPIPE信号:13号信号

2.4.2 定时器软件条件

#include <unistd.h>

unsigned int alarm(unisegned int seconds);

参数:seconds:秒数 -- 若为0,则意味着取消闹钟

返回值:提前唤醒时,会返回剩余秒数

告诉内核在seconds秒后,给当前进程发SIGALRM信号

#include <iostream>    #include <signal.h>    #include <unistd.h>    #include <sys/types.h>    #include <string>    using namespace std;int main(int argc, char *argv[])    {  int cnt = 0;alarm(1);while(1){cnt++;cout << "cnt = " << cnt++ << endl; // 外设打印 拖慢了计算的节奏}
return 0;                                                    } 

 这段代码帮我们统计了计算机在一秒内可以累加并打印多少次。

#include <iostream>    #include <signal.h>    #include <unistd.h>    #include <sys/types.h>    #include <string>    using namespace std;int cnt = 0;    void catchSignal(int signo)    {    cout << "获得一个信号,信号编号为:" << cnt << endl;    // exit(1);    }  int main(int argc, char *argv[])    {  alarm(1);while(1){cnt++;}
return 0;                                                    } 

这段代码为何与前一段代码打印出来的结果相差如此之大呢,是因为计算机进行打印(IO)是很费时的。

2.5 小结

  • 所有的信号产生方式都是由OS来执行的,因为OS是进程的管理者

  • 信号并不是立即处理的,是在合适的时间

  • 信号需要被保存在PCB中

  • 进程若没有收到信号的时候,进程就知道如何对信号处理

  • OS向进程发信号就是OS修改目标进程的PCB中的信号位图 

3. 进程退出时的核心转储问题

进程退出时有两种方式:Term、Core,以core方式退出的进程可以利用核心转储来快速定位错误

ulimit -a : 用于显示当前shell的各种资源限制(ulimits)

ulinit -c 1024, 打开云服务器的core file选项,将size设置为1024

demo代码:

#include <iostream>    #include <signal.h>    #include <unistd.h>    #include <sys/types.h>    #include <string>    int main()
{while(1){int a[10];a[10000] = 106;}return 0;
}

 当打开core file选项前:

  当打开core file选项后结果为下,并且当前目录下会生成一个core.pid文件:

 core dumped:核心转储—当进程出现异常时,我们将进程在对应的时刻,在内存中的有效数据转储到磁盘中;那么它存在的意义是什么呢?为了支持调试,如何支持?--gdb 文件

在gdb上下文中输入: core-file core.pid   即可找到异常位置 ,如下图所示,就能找到上面代码中的问题。这种方式称为事后调试

4. 信号捕捉初识

前文提到过,信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的

#include <signal.h>

sighandler_t signal(int signum, sighandler_t handler);

参数:signum:信号编号或宏定义

           handler:回调函数,用来如何处理这个信号

#include <stdio.h>    
#include <signal.h>    
void handler(int sig)    
{    printf("catch a sig : %d\n", sig);    
}    
int main()    
{    signal(2, handler); //前文提到过,信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的,提了解一下                                                                                 while(1);    return 0;    
}   

可以看到,一开始,代码进入死循环,当我们在命令行按 ctrl+c 时,进程不会退出,而是执行了我们自己定义的动作。-- 信号处理的自定义动作

对所有的[1, 31]信号捕捉后,是否这个进程就无法杀死了呢? -- 不会 kill -9 pid 会杀死任意进程

5. 阻塞信号

5.1 相关概念

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

5.2 在内核中的表示

block(block又称为信号屏蔽字)中位置内容为1的信号不会被递达,除非阻塞解除。即使没有收到某一个信号,也可以将该信号设置为阻塞状态--设置block表。若一个信号在产生前被设置为阻塞状态,当该信号产生后,不会被递达,直到阻塞解除。下面为一个伪代码:

在内核中除了两个位图外还有:typedef void(*handler_t)(int signo); -- 函数指针
handler_t handler[32] -- 数组内容为指针,指向对每个信号的处理方法(函数)。

结论: 

  • 如果一个信号没有产生,并不妨碍其先被阻塞
  • 进程通过三种结构的结合来识别信号
  • POSIX.1允许系统递送该信号一次 或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。

6. 信号捕捉

6.1 知识铺垫

用户态:处于用户态的 CPU 只能访问受限资源,不能直接访问内存等硬件设备,不能直接访问内存等硬件设备,必须通过「系统调用」陷入到内核中,才能访问这些特权资源。

内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态

用户为了访问内核或者硬件资源,必须通过系统调用完成

用户无法以用户态的身份执行系统调用,那么执行系统调用的是进程,但是身份是内核态身份

(系统调用是比较费时的,应尽量避免频繁的系统调用)

CPU中存在指向页表的寄存器、指向task_struct的寄存器、CR3寄存器:表征当前进程的运行级别,0内核态,3用户态

看上图,OS中还有唯一的一个内核级页表,将不同进程的内核空间映射到物理内存的同一块区域,那么访问OS的接口,只需要在自己的地址空间进行跳转就可以了

6.2 信号捕捉流程

信号在产生时,不是被立即处理的,是从内核态返回用户态的时候进行处理的,那么是什么时候进入的内核态呢?-- 系统调用/进程切换 

信号捕捉流程如下:

  • 进程由于中断/异常进入内核态返回用户态之前会检查当前进程PCB中的block、pending、handler表;
  • 先查看blockblock表若blok为1无论是否产生信号都不处理,直接返回;为0,继续检查pending表,查看信号是否产生;
  • 若一个位置block为0,pending为1,则继续查handler表匹配的方法,执行对应的处理方法;
  • 若handler表中的方法是自定义方法,由于自定义方法处于用户态,此时进程还要通过特定的调用从内核态变为用户态执行对应的方法(注意这里是无法在内核态执行用户态代码的)
  • 执行完处理方法后,再返回内核态(不能直接执行完处理方法后直接返回带用户态的代码处),继续返回到用户态执行到的对应的代码处

 如果信号的处理方法为自定义的那么一定涉及到四次状态的切换

6.3 sigset_t

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

6.4 信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set); // 初始化,将所有信号对应的比特位清0
int sigfillset(sigset_t *set);        // 初始化,将所有信号对应的比特位置1 
int sigaddset (sigset_t *set, int signo);  // 将signo信号的比特位置为1
int sigdelset(sigset_t *set, int signo);    // 将signo信号的比特位置0
int sigismember(const sigset_t *set, int signo);  // 判断set中是否包含signo信号

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

参数:how:指示如何更改

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

                      SIG_UNBLOCK:mask = mask & ~set  -- set包含了我们希望从当前信号                                                                                                  屏蔽字中解除阻塞的信号

                      SIG_SETMASK:mask = set -- 设置当前信号屏蔽字为set所指向的值

           set:按照how更改信号屏蔽字

           oset:读取当前进程的信号屏蔽字

返回值:成功返回0,失败返回-1

int sigpending(sigset_t *set); // 获取当前进程的pending位图/未决信号集

成功返回0,失败返回-1

对上述接口使用的demo代码:

#include <iostream>    
#include <unistd.h>    
#include <signal.h>    
#include <vector>    #define MAXSIGNUM 31    using namespace std;    static vector<int> sigarr = {2};    void printPending(sigset_t *pending)    
{    for(int i = MAXSIGNUM; i >= 1; i--)    {    if(sigismember(pending, i)) cout << "1";    else cout << "0";    }    cout << endl;    
}    int main()    
{    // 1.先尝试屏蔽指定的信号    sigset_t block, oblock, pending;    // 1.1 初始化    sigemptyset(&block);  // 将位图结构中都置为0                                                                                                                                             sigemptyset(&oblock);  // 将位图结构中都置为0    sigemptyset(&pending);  // 将位图结构中都置为0    // 1.2 添加要屏蔽的信号    //for(const auto &e:sigarr)     sigaddset(&block, 2);    // 1.3 开始屏蔽    sigprocmask(SIG_SETMASK, &block, &oblock);    // 2.遍历打印pending的信号集    int cnt = 10;    while(1)  {    // 2.1 初始化pending信号集sigisemptyset(&pending);// 2.2 获取当前进程的未决信号集sigpending(&pending);// 2.3 打印printPending(&pending);sleep(1);if(cnt-- <= 0){cout << "恢复对信号的屏蔽,不屏蔽任何信号\n" << endl;sigprocmask(SIG_SETMASK, &oblock, &block); // 一旦解除信号的屏蔽,且此时信号已经处于未决状态时,会立马把该信号递达,执行对应的处理动作}}return 0;
}

这段代码实现的功能是,阻塞2号信号(ctrl+c),打印pending表,在10s后解除对所有信号的屏蔽。

若在10s内收到2号信号时,不会直接处理,在10s后解除对2号信号的屏蔽后,执行2号信号的处理方法,默认为终止程序。结果如下:

6.5 sigaction

struct sigaction:

其中sigset_t sa_mask:当正在处理某种信号时,想顺便屏蔽其他信号,就可以添加到这个                                                   sa_mask 

int sigaction(int signo, const struct sigaction* act, struct sigaction* oact);

参数:signo:

           act:输入性参数

           oact: 输出型参数:获取对应信号旧的处理方法

返回值:成功返回0,失败返回-1

#include <iostream>    
#include <signal.h>    
#include <cstdio>    
#include <unistd.h>    using namespace std;    void Count(int cnt)    
{    while(cnt)    {    printf("cnt: %2d\r", cnt--);    fflush(stdout);    sleep(1);    }    cout << endl;    
}    void handler(int signo)    
{    cout << "正在处理" << signo << "号信号" << endl;                                                                                             Count(20);    
}    int main()    
{    struct sigaction act, oact;    act.sa_handler = handler;    act.sa_flags = 0;    sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 3);sigaction(SIGINT, &act, &oact);// sigaction(3, &act, &oact);while(1){cout << "我是一个进程" << endl;sleep(1);}return 0;
}

上述代码想要实现的功能是,在处理2号信号期间同时屏蔽掉3号信号

当我们在递达一个信号期间,同类型的信号无法被递达。

        当前信号正在被捕捉,系统会将当前信号加入到进程的信号屏蔽字block,完成捕捉动作,系统又会自动解除对该信号的屏蔽。一般一个信号被解除屏蔽时,如果该信号已经被pending,会自动递达当前信号

        当某个信号的处理函数被调用时, 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时, 除了当前信号被自动屏蔽之外 , 还希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字

7. 可重入函数

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因 为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函 数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从 sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数;反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数

(1).一般而言,认为main执行流和信号捕捉执行流是两个执行流

(2).如果在main中和在sighandler中,某函数被重复的进入,出问题--该函数为不可重入函数;若未出问题则为可重入函数 

可重入/不可重入不是一个问题,也不需要解决。目前大部分情况都为不可重入函数

如果一个函数符合以下条件之一则是不可重入的:
  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

8. 关键字volatile

关键字volatile:保持内存可见性,避免编译器优化导致的错误

#include <stdio.h>                                                   #include <signal.h>    volatile int quit = 0;    void handle(int signo)    {    printf("%d号信号,正在被捕捉!\n", signo);    printf("quit: %d", quit);    quit = 1;    printf(" -> quit: %d", quit);    }    int main()    {    signal(2, handle);    while (!quit)    {    printf("正在循环!\n");    sleep(1);    };    printf("注意,我是正常退出的!\n");    return 0;    }  

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

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

相关文章

CTFHUB-SSRF-上传文件

通过file协议访问flag.php文件内容 ?urlfile:///var/www/html/flag.php 右键查看页面源代码&#xff0c;发现需要从内部上传一个文件这样才能正常获取到flag ?urlhttp://127.0.0.1/flag.php 发现无提交按钮&#xff0c;构造一个 <input type"submit" name&qu…

RabbitMQ 之 延迟队列

目录 ​编辑一、延迟队列概念 二、延迟队列使用场景 三、整合 SpringBoot 1、创建项目 2、添加依赖 3、修改配置文件 4、添加 Swagger 配置类 四、队列 TTL 1、代码架构图 2、配置文件代码类 3、生产者 4、消费者 5、结果展示 五、延时队列优化 1、代码架构图 …

Python番外篇之责任转移:有关于虚拟机编程语言的往事

编程之痛 如果&#xff0c;你像笔者一样&#xff0c;有过学习或者使用汇编语言与C、C等语言的经历&#xff0c;一定对下面所说的痛苦感同身受。 汇编语言 将以二进制表示的一条条CPU的机器指令&#xff0c;以人类可读的方式进行表示。虽然&#xff0c;人类可读了&#xff0c…

使用deep修改前端框架中的样式

目录 1.deep的作用 2.使用方式 3.特别说明 scoped 的实现原理&#xff1a; !important 1.deep的作用 /deep/、::v-deep 和 :deep 都是用于穿透组件作用域的选择器。它们的主要目的是允许开发者在父组件中直接选择并样式化子组件内部的元素&#xff0c;即使这些元素被封装在…

JVM原理(九):JVM虚拟机工具之可视化故障处理工具

1. JHSDB:基于服务性代理的调试工具 JHSDB是一款基于服务性代理实现的进程外调试工具。 服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言实现的API集合。 2. JConsole:Java监视与管理控制台 JConsole是一款基于JMX的可视化监视、管理工具。…

千益畅行,旅游卡,如何赚钱?

​ 赚钱这件事情&#xff0c;只有自己努力执行才会有结果。生活中没有幸运二字&#xff0c;每个光鲜亮丽的背后&#xff0c;都是不为人知的付出&#xff01; #旅游卡服务#

springboot拦截器,ThreadLocal(每个线程的公共区域)

拦截器 配置信息&#xff08;拦截所有请求&#xff09; 其实这种可以作为springAOP作日志记录

SpringCloud_Eureka注册中心

概述 Eureka是SpringCloud的注册中心。 是一款基于REST的服务治理框架&#xff0c;用于实现微服务架构中的服务发现和负载均衡。 在Eureka体系中&#xff0c;有两种角色: 服务提供者和服务消费者。 服务提供者将自己注册到Eureka服务器&#xff0c;服务消费者从Eureka服务器中…

软件著作权申请:保障开发者权益,促进软件创新

一、引言 在数字化时代&#xff0c;软件作为信息技术的核心&#xff0c;已成为推动社会进步和经济发展的重要力量。然而&#xff0c;随着软件产业的蓬勃发展&#xff0c;软件侵权和抄袭现象也日益严重。为了保护软件开发者的合法权益&#xff0c;促进软件产业的健康发展&#…

摄影后期色彩管理流程(Lightroom篇)

在摄影后期处理中&#xff0c;色彩管理是确保图像从捕捉到输出的一致性和准确性的关键。Lightroom 和 Photoshop 其实已经将这套色彩管理流程作为默认选项&#xff0c;如果实质操作时仍存在色彩偏差的问题&#xff0c;可参考以下内容。 ProPhoto RGB > Adobe RGB > sRGB …

linux 控制台非常好用的 PS1 设置

直接上代码 IP$(/sbin/ifconfig eth0 | awk /inet / {print $2}) export PS1"\[\e[35m\]^o^\[\e[0m\]$ \[\e[31m\]\t\[\e[0m\] [\[\e[36m\]\w\[\e[0m\]] \[\e[32m\]\u\[\e[0m\]\[\e[33m\]\[\e[0m\]\[\e[34m\]\h(\[\e[31m\]$IP\[\e[m\])\[\e[0m\]\n\[\e[35m\].O.\[\e[0m\]…

Golang内存分配

Go内存分配语雀笔记整理 Golang内存模型设计理念思考核心代码阅读mspanmcachemcentral中心缓存mheap分配过程 Golang内存模型设计理念思考 golang内存分配基于TCmalloc模型&#xff0c;它核心在于&#xff1a;空间换时间&#xff0c;一次缓存&#xff0c;多次复用&#xff1b;…

HarmonyOS开发探索:父子组件手势绑定问题处理

场景一&#xff1a;父子组件同时绑定手势的冲突处理 效果图 方案 在默认情况下&#xff0c;手势事件为非冒泡事件&#xff0c;当父子组件绑定相同的手势时&#xff0c;父子组件绑定的手势事件会发生竞争&#xff0c;最多只有一个组件的手势事件能够获得响应&#xff0c;默认子…

二、基础—常用数据结构:列表、元祖、集合、字典、函数等(爬虫及数据可视化)

二、基础—常用数据结构&#xff1a;列表、元祖、集合、字典、函数等&#xff08;爬虫及数据可视化&#xff09; 1&#xff0c;字符串2&#xff0c;最常用的是列表&#xff08;重点掌握&#xff09;3&#xff0c;元组4&#xff0c;字典&#xff08;重要&#xff09;5&#xff0…

【CSS in Depth 2 精译】2.3 告别像素思维

当前内容所在位置 第一章 层叠、优先级与继承第二章 相对单位 2.1 相对单位的威力 2.1.1 响应式设计的兴起 2.2 em 与 rem 2.2.1 使用 em 定义字号2.2.2 使用 rem 设置字号 2.3 告别像素思维 ✔️2.4 视口的相对单位2.5 无单位的数值与行高2.6 自定义属性2.7 本章小结 2.3 告别…

3D交互可视化编辑器求推荐,最好是针对企业级使用的?

企业级使用的3D交互可视化编辑器&#xff0c;支持编辑和调整2D、3D渲染及交互设置&#xff0c;以下几款可以关注了解一下&#xff1a; 1、Unity&#xff1a;一个广泛使用的跨平台游戏引擎&#xff0c;由Unity Technologies开发。支持开发者创建2D和3D游戏、交互式应用以及虚拟…

pdf压缩,pdf压缩在线网页版,在线压缩pdf网站

在数字化时代&#xff0c;pdf文件已经成为我们工作、学习和生活中不可或缺的一部分。然而&#xff0c;pdf文件往往体积庞大&#xff0c;传输效率低下&#xff0c;还占用大量存储空间。如何在不影响文件质量的前提下&#xff0c;减小pdf文件的大小呢&#xff1f;今天&#xff0c…

74HC595芯片验证

目录 0x00 74595芯片简介0x01 实现原理 0x00 74595芯片简介 74595芯片有很多种封装&#xff0c;不管是贴片的还是直插式的&#xff0c;它们的引脚定义都如下图所示&#xff1a; 其中 &#xff1a; Q0-Q7为并行数据输出口&#xff0c;可以输出高低电平。OE 为使能引脚&#…

MySQL之高可用性和应用层优化(一)

高可用性 故障转移和故障恢复 在应用中处理故障转移 有时候让应用来处理故障转移会更加简单或者更加灵活。例如&#xff0c;如果应用遇到一个错误&#xff0c;这个错误外部观察者正常情况下是无法察觉的&#xff0c;例如关于数据库损坏的错误日志信息&#xff0c;那么应用可…

Hugging Face发布重量级版本:Transformer 4.42

Hugging Face 宣布发布Transformer 4.42&#xff0c;该版本为流行的机器学习库带来了许多新功能和增强功能。此版本引入了几个高级模型&#xff0c;支持新工具和检索增强生成 &#xff08;RAG&#xff09;&#xff0c;提供 GGUF 微调&#xff0c;并整合了量化的 KV 缓存&#x…