当前位置: 首页 > news >正文

Linux相关概念和易错知识点(25)(信号原理、操作系统的原理、volatile)

目录

1.信号的产生

(1)kill

(2)raise、abort

2.对block、pending、handler表的管理

(1)信号集(sigset_t)

(2)block表的管理

①操作相关的函数

②sigprocmask

(3)pending表的管理

(4)handler表的管理

3.操作系统的原理

(1)硬件中断

①中断控制器

②中断号

③保护现场、恢复现场

(2)时钟源

①主频

②时间片

(3)异常处理

(4)软中断

①软中断和硬件中断

②系统调用表

③缺页中断

④异常、陷阱

(5)用户态和内核态(重点)

①用户区、内核区

②用户态和内核态之间的切换

③函数跳转

(6)捕捉信号流程

①用户态和内核态的切换

②对状态切换的理解

③对执行流概念的深化

(7)volatile

①可重入函数

②volatile、编译器优化

4.信号拓展

(1)SIGCHLD

(2)SIGALRM


1.信号的产生

(1)kill

信号究竟是如何产生的?我们已经知道信号的各种上层规则,但对于信号的来源还不是很熟悉。

看一下下面的代码,就能很快理解了。

kill本身就是个系统调用,而不仅仅是一个指令int kill(pid_t pid, int sig);就可以实现对特定的pid进程发送sig信号,当信号发送成功时返回0,失败时返回-1。

也就是说所谓的kill指令,底层还是kill函数系统调用(指令->系统调用)。因此我们可以理解,当bash进程要kill掉其子进程时,就是调用的kill函数实现的。如当管道的读端关闭,系统会直接杀掉进程,就是使用的kill发送SIGPIPE信号

(2)raise、abort

int raise( int )意思是谁调用,就给自己发送这个信号,也就是说raise(9)可以杀掉自己。

void abort( void )意思是谁调用,就给自己发送SIGABRT 6号终止信号。相当于raise(6)

2.对block、pending、handler表的管理

在介绍了三张表的功能和调用流程之后,我们需要进一步讲讲如何修改这三张表,因为信号从接收到发送的全过程都由这三张表控制,管理这三张表本质上就是在管理信号的处理。

(1)信号集(sigset_t)

未决(pending)和阻塞(block)表都可用相同类型来存储,因为它们本质都是位图。这个位图的结构体是sigsei_t,也称为信号集这个类型在两张表有不同含义:在pending表表示是否有接收到该信号,在block表表示是否阻塞该信号阻塞(block)信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的屏蔽是指阻塞。

我们只需记住信号集的本质是位图,是未决(pending)和阻塞(block)表中位图的专属类型。 

(2)block表的管理

注意以下的函数都是针对block表的信号集!

①操作相关的函数

首先,我们需要自己创建一个sigset_t的变量,再对这个位图进行如下处理:

int sigemptyset(sigset_t *set);将位图清0; int sigfillset(sigset_t *set);将位图填为全1

int sigaddset(sigset_t *set, int signum);添加对signum号信号的屏蔽(本质就是修改位图,将对应位改为1);int sigdelset(sigset_t *set, int signum);删除对signum号信号的屏蔽

上述返回值都是0为成功,-1为错误

②sigprocmask

接下来我们要让我们的改动生效,上述所有的改动都是用户自己的修改,并没有写到内核中去。

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

how由如下宏定义决定功能:SIG_BLOCK增加传入的newset中状态为1的信号的阻塞;SIG_UNBLOCK解除传入的newset中状态为1的信号的阻塞;SIG_SETMASK直接用newset覆盖block表中的位图。注意这个newset就是刚才我们进行各种处理后的sigset_t变量(输入型参数)。oldset是输出型参数,是修改前的位图,帮助我们恢复原来的位图。

除了上述用法之外,sigprocmask(0, NULL, &oldset)可以获取当前的block表

下面的代码,我利用阻塞表阻塞了2号信号的处理,使得信号无法被处理

(3)pending表的管理

int sigpending(sigset_t *set);用于获取pending表,set是一个输出型参数,该函数只提供内核中的pending表而不提供修改。其返回值0表示成功,-1表示失败。

int sigismember(const sigset_t *set, int signum); 判断signum对应信号是否有效。pending和block表都可以用,因为它们的位图的数据类型一致。我们需要手动传入sigset_t,这需要我们使用sigpromask、sigpending获取当前block、pending表。其返回值是1为真,0为假,-1为错误

(4)handler表的管理

signal函数可以进行信号捕捉,进而修改handler表,这里提一句即可。

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

对于这个结构体,通常flag设置为0,sa_handler设置为指定的void(int)函数指针,使用为sigaction(2, &act, &oldact),即将信号2的默认处理方式替换为自定义处理,并且得到一个oldact用于备份。

struct sigaction的sa_mask成员还可以顺便帮我们添加要屏蔽的信号,我们自己设置sigset_t,在后续调用sigaction函数时,除了相应pending表被修改,block表也会加上sa_mask里面的几个屏蔽的信号。

3.操作系统的原理

要进一步理解信号,我们要对OS进一步深挖,来解释为什么我们键盘输入信号后OS能够及时的处理?是否需要OS一直等待键盘输入?同步异步是如何实现的?

(1)硬件中断

OS怎么知道键盘输入数据了?是否需要OS一直等待键盘输入?事实上,OS是启动的第一个软件,它不会轮询设备,设备数量太多了,键盘、磁盘、显示屏等,而是外设提醒OS后OS再来处理。

①中断控制器

以键盘输入为例,键盘按下会首先触发硬件中断(由硬件电路实现),这个硬件中断的信号不会直接传给CPU(物理上键盘并没有直连CPU),而是传给中断控制器(和每个设备直连),中断控制器再向CPU发送信息,进而被OS获取。

注意中断控制器可不会关心键盘按什么键,它只会告诉CPU发生了中断,CPU也不会关心,它收到中断后只会告诉OS有外设准备好了,之后在OS的管理下,键盘给CPU传信息,进而实现给OS传信息。至于键盘输入了什么,那是OS之后需要干的事

再以磁盘为例,当磁盘想要给进程发信息时,虽然磁盘的寻址需要时间,要定位要准备,但这段时间OS不管,继续执行自己的任务,仅当磁盘准备好后中断控制器直接向CPU发送信号,执行读取操作即可。这样,硬件和OS实现了并行执行。

这里我们就会发现,信号和硬件中断有相似之处,信号是纯软件模拟中断的行为,硬件中断是靠软硬结合实现的(绝大部分是硬件)。

②中断号

对于中断控制器而言,每个设备都对应一个中断号,中断控制器利用中断号触发高电平告诉CPU谁中断了。这些中断号OS明白对应哪些设备,此时OS就回去调用对应的处理方法。要管理这些处理方法,OS有一个中断向量表IDT,这是一个函数指针数组,中断号就相当于数组下标。当中断控制器告诉OS中断号后,OS直接由IDT调用中断服务处理中断。这些中断服务包括读取硬盘、网卡等。方法内容由OS自定或安装驱动确定,同时我们也要知道中断号和中断处理方法是相对固定的,是软硬结合共同维护的。

③保护现场、恢复现场

在OS接收到中断控制器的信号,确定要处理中断之后,OS会将当前CPU各种寄存器的数据保存在中断的上下文,方便后续处理。保存好寄存器数据后,OS就会根据中断向量表找方法,执行中断处理例程(利用中断号查表)。处理完后就会恢复现场,继续执行任务。

OS通过中断实现了不主动轮询外设。所有外设(不含内存,它是存储器)都是这么处理的。

(2)时钟源

进程可以在OS指挥下调度执行,那么OS自己被谁指挥呢?时钟源。

①主频

我们已经知道了所有外设的信息是如何进入进程被处理的了,其中有一个外设,每隔一段极短时间就会给CPU发硬件中断,这个外设就是时钟源,它具有一个固定的中断号,以及一个固定的中断服务:进程调度。时钟源会一直推动OS进行进程调度,OS就是基于不断中断,调用中断向量表工作的。现在时钟源已经被集成在CPU内部,时钟源的触发频率就是CPU主频的概念(如14900K的6GHZ)。主频速度越快,处理任何操作都会更频繁。

OS在时钟的推动下自行调度,相当于说OS可以不做任何事,启动之后直接让自己进入死循环,利用时钟和其它外设触发中断推着它进行运行,执行方法。

总结:对于OS来说,只要完全没有进程,它就陷在死循环中,但只要有进程,就会一直忙着调度,这是时钟源的中断推着它进行的。我们因此可以说操作系统就是躺在中断中运行的。与此同时,OS也会自己去fork一些进程,这是内核固定进程,会定期去检查OS运行状态,定期把内核缓冲区数据进行刷新、检查闹钟等操作。 

②时间片

时钟中断是固定时间的,假设为1ns。进程的调度都设置了时间片,轮询着调度以尽量保持公平。OS要实现时间片,只需设置int count,当count == 1000(1微秒),每次时钟源中断触发进程调度都会让count--,count减到0后直接切换进程。

时间片就是在主频下的计数器,是以时钟中断为基础构建的。

(3)异常处理

我们已经知道,程序的崩溃就是靠信号终止的。

其中野指针触发段错误,操作系统直接用11号信号杀掉进程,我们捕捉信号后发现OS一直在触发信号。

同理,a / 0也会崩溃,会触发13号信号SIGFPE终止进程。

为什么OS知道我们的进程的内部出错了,为什么OS会一直触发信号?

以a / 0为例,CPU中有寄存器,a -> eax,0 -> ebx, 计算结果 -> ecx。同时还有个状态寄存器Eflags,有溢出标记位,默认为0,保证会把结果正常返回。当溢出标记位为1,OS就知道CPU硬件内部出错了,OS找到对应的进程后就要用信号杀掉损坏CPU的进程。

当我们捕获信号之后,我们的进程没有退出,直到时间片到了,进行进程切换时会OS会保存寄存器数据,以便下次调用恢复。其中Eflags的标记位也被保存了,OS不会修改它。由此以来,每当调度到这个进程时,CPU都会显示异常,OS会一直尝试杀我们的进程,这导致了死循环调用信号处理。

同理,OS怎么判断野指针呢?CR3寄存器保存页表起始地址,虚拟 -> 物理地址的转换是MMU内存管理单元操作的,它集成在CPU中。当访问野指针(虚拟地址)时,MMU转换去读0,发现无法访问,于是触发了硬件错误,OS知道后发送信号尝试杀掉进程。当后续再轮换调用时,MMU的错误被完整地继承了下来,所以OS会循环发送信号。

(4)软中断

①软中断和硬件中断

上述都是外部硬件中断,需要硬件设备触发。有没有仅靠软件进行中断的呢?

为了让OS支持系统调用,CPU专门设计了对应的汇编指令(int或者syscall),在没有外设下,可以让CPU内部触发中断逻辑。这些汇编指令就可以写在软件中,通过汇编指令 + 中断号推动CPU执行中断向量表方法,这就叫软中断。硬件中断和软中断仅仅是触发方式变为汇编调用,其余流程一致,这很好理解。

两种触发方式最终都是利用中断号调用中断向量表中的函数。例如,int N 指令会触发中断号为 N 的软中断,操作系统会根据该中断号来找到对应的处理函数(范围从 0x00 到 0xFF(即从 0 到 255))

②系统调用表

系统调用也是通过中断完成的。OS系统调用都在系统调用表,集中管理,是由“下标”调用的。系统调用表特别底层且固定,用户无法查看这个表的存在。这个表中各个函数的下标叫系统调用号,在中断向量表中设置一个系统调用的入口函数,用系统调用号调用。

要调用系统调用,就要想办法软中断,int 0x080就是软中断到执行系统调用入口函数,进入固定例程。系统调用号会提前写到寄存器中,可以直接被获取。

系统调用的过程:先把要调用的系统调用号用寄存器存起来,再触发软中断陷入内核,OS根据中断向量表开始进入中断服务,根据寄存器里的系统调用号自动查系统调用表,执行对应方法。

OS提供的系统调用接口根本不是C函数,而是系统调用号 + 约定传递的参数,通过触发软中断、进行中断服务调用的方式实现系统调用功能的。系统调用的真正的底层实际上是汇编。

GNU、glibc给系统进行C语言封装,给我们提供C语言的系统调用。所以我们用的所有系统调用都是C的,是它对该平台的系统调用进行了统一的上层封装,这也是C具有跨平台性的本质。C语言的上层封装还保证了我们系统调用的安全性,不会导致对系统造成伤害。

③缺页中断

缺页异常意思就是有虚拟地址,但物理内存没有申请(分批加载)。当OS需要用到相应空间,但发现还没有物理内存时,就会触发中断,申请并加载物理内存

④异常、陷阱

OS就是躺在中断例程上的代码块,几乎所有操作都会转为中断。缺页中断、内存碎片处理、除零、野指针错误都是转为软中断,OS会设置中断号走中断例程。异常、陷阱都会转为软中断来处理。

如果发生了软中断且不是因为出错,就是单纯为了陷入中断(系统调用),这叫做陷阱

而像除零、野指针这种触发错误导致软中断的叫做异常。

OS会根据不同错误或者陷阱由中断号进行调用。

(5)用户态和内核态(重点)

①用户区、内核区

在32位操作系统下,进程的地址空间划分了4G,其中这块虚拟的地址空间包含了我们已知的堆栈段、代码段、数据段、以及命令行参数列表等。所有的这一切所在的内存区域都属于用户区。用户区从0G ~ 3G,共3G的大小。

3G ~ 4G这块叫做内核区,这块内核区也是虚拟的,它拥有共同的内核页表,同用户页表一样,都是映射到物理内存中的。不过需要注意的是,不同进程的内核区都会映射到同一块物理内存。

这里需要注意的是,用户页表每个进程一份,因为虚拟地址和物理地址都要根据不同进程实际情况映射。;而内核页表则是整个系统一份,无论是虚拟地址还是映射的物理地址都是同一份。对于任何进程来说,无论如何调度,它们都能找到同一个OS,访问同一张中断向量表和系统调用表。

无论调用任何函数(库、系统调用),都是在我们自己用户区进行调用的(代码段在用户区)。而被调用的系统调用方法的执行是在内核区进行的。

本质上说,内核区映射到同一块空间是因为OS只有一个,那么如果使得进程映射多块空间,就相当于启动了两个系统,这就是内核虚拟机的思路,当然还有用户虚拟机,这里提一句。

②用户态和内核态之间的切换

前面我们已经知道用户区和内核区了,这两个区域只能被具有对应的身份的人访问,标记身份的就是用户态和内核态。

如果我们要进入内核区,我们需要将我们的身份进行改变,即用户态 -> 内核态;同理,如果要访问用户区,我们也应该将内核态 -> 用户态。 

标记用户态和内核态的是CPU的CS段寄存器。它有两个bit位的标志(CPL):00表示内核,11表示用户。修改用户内核态的本质就是修改标志位。我们都知道MMU是集成在CPU内部,负责进行虚拟地址 -> 物理地址的,只要CPL不允许访问,MMU也自然不会给我们转换地址,我们也自然访问不了对应的空间。用户态和内核态的权限管理是硬件层面的。

当我们在用户区调用系统调用时,会使用int (中断号) 和 syscall (中断号) 触发软中断,进到中断向量表里面去执行系统调用入口函数,进而访问系统调用表,执行系统调用方法,最后回到调用处继续执行代码。在整个过程中,当触发软中断时就会第一次切换状态,将权限标志位CPL改为0,进入内核态。执行完后返回调用处又会切换一次,CPL改为3,进入用户态。

③函数跳转

在上述流程中,我们会有疑问:函数是如何跳转的,跳转回来时如何找到最开始的地址的?A函数调用B,A会把调用B的下一个地址先入栈,后面出栈后就能够找到下一句地址。由此以来就能正确地在内核区和用户区之间进行跳转。汇编层面所有操作都是基于寄存器、地址的。

(6)捕捉信号流程

有了前面知识的积累,我们能够更底层地理解信号捕捉的流程了。

①用户态和内核态的切换

如果来了一个信号,有可能当前进程正在做更重要的事,我们要把信号保存到pending表中。当进程从内核态切换回用户态的时候,进程就会进行信号检查do_signal(),检测当前的pending和block表决定是否处理信号。

当要处理信号时,会根据不同处理信号的方式决定走向。如果是DFL和IGN的情况,那么进程会不急着返回用户态,会在内核态把方法执行完成后再返回(DFL和IGN的代码都在内核区);如果我们自定义捕捉了信号,这些代码都在用户区保存,所以我们要切换回用户态执行处理代码处理之后,会回到内核态,执行sys_sigreturn()返回主程序,回到用户态

②对状态切换的理解

为什么执行自定义函数时要做权限切换?直接用内核态执行不行吗?自定义函数存在用户区,需要用户态,而内核态权限更高,切换回用户态是为了避免安全风险,防止内核态被利用。同时内核态进入用户态后执行自定义函数,意味着用户态的操作后果自负!

③对执行流概念的深化

我们执行完自定义函数后,想要回到主程序。为什么不直接回去,反而还要进入内核态呢?实际上自定义函数和主程序没有任何关系,在程序中不存在任何调用关系,信号处理和主程序是两个完全不同的执行流,因此只能先返回到内核。当触发硬件中断,OS会保护现场,存储各个寄存器的状态。其中pc寄存器就保存下一条指令的地址,在这里就是主执行流的下一条指令的地址。因此回到用户态,需要在内核态调用sys_sigreturn()函数,恢复上下文,才可以回到初始时的在主程序执行流。

到这里,我们也能彻底理解信号处理没有新开一个进程,主执行流和信号捕捉执行流,信号捕捉执行流没有在主执行流中被调用,而只是一个主执行流中的分支,进程调用过程中两个分支不会冲突。

下面是执行流切换的过程,仔细体会

(7)volatile

①可重入函数

若一个函数被两个以上执行流同时进入,就有可能遇到下面这种情况,这是单执行流的情况下遇不到的。


因此实例的insert函数是不可重入函数。相对的,还有可重入函数,也就是可以被被两个以上执行流同时进入同时不发生错误的函数。怎么判断呢?

只要使用了全局资源的,new了空间的,基本都是不可重入的,使用的都是局部变量的就可能是可重入的基本上STL都不可重入,大部分函数也都不可重入。但也有专门设计的系统接口带_r,表示可重入。

volatile、编译器优化

gcc有优化选项 -O0基础优化,-O1,-O2,-O3优化级别依次增加。

对于主程序执行流和信号处理执行流来说,由于它们从语法上没有任何联系,但实际上可以通过全局资源联系,编译时就有可能被优化出bug。

若在主执行流中对一个全局变量没有修改,而实际上这个全局变量在信号捕捉执行流中被处理。对于高优化的编译器来说,这个全局变量在信号捕捉执行流中的处理无效。我们要从底层来理解优化。

当编译器优化很大时,一些主执行流没有修改的变量会变成寄存器变量,这个变量永远无法被修改。当我们使用这个变量来进行判断时,逻辑判断会直接用寄存器里面的值进行判断而不会使用内存的数据这就导致信号捕捉执行流中修改变量不会生效,寄存器 + 优化屏蔽了内存的可见性。

如果担心被优化导致出现bug,我们可以使用volatile关键字修饰全局变量,volatitl int flag = 0;就不会被寄存器屏蔽了,这个关键字相当于告诉编译器不要对该变量进行任何优化,以保持内存可见性

4.信号拓展

(1)SIGCHLD

父进程一般关心子进程什么时候死,这样好回收,以免出现孤儿或者僵尸。事实上,子进程退出时都会向父进程发送SIGCHLD信号,告诉父进程自己结束了。不过默认情况下SIGCHLD的Action是Ign(忽略)。我们可以手动捕获这个信号,当父进程收到信号时再去wait,这样就能实现父子进程异步执行,而不是让父进程一直阻塞在原地。

但是有个问题,即信号处理过程中最多允许再接收一个信号,后续的没意义(pending已经为1了),所以每次需要waitpid + WNOHANG循环,用返回值来判断是没等完还是等完了。

仅Linux下,signal(SIGCHLD, SIG_IGN)处理后,这样fork出来的子进程在终止时会自动清理掉,没有僵尸,不过父进程也得不到status。这里可认为是Linux的特殊处理,仅仅用来处理僵尸的情况。

(2)SIGALRM

alarm(seconds)函数可以设置一个一次性闹钟信号(只会响一次,重复使用需要多次调用),闹钟时间到了之后会发送一个14号SIGALRM信号,默认就是Term终止。我们可以捕获它,利用alarm写一个统计服务器1s执行某种操作多少次的代码,可以用alarm来对比IO对效率的影响有多大等操作。

alarm(0)取消闹钟,其返回值是闹钟剩余时间。如果闹钟自己响了,那返回值就是0。

相关文章:

Linux相关概念和易错知识点(25)(信号原理、操作系统的原理、volatile)

目录 1.信号的产生 (1)kill (2)raise、abort 2.对block、pending、handler表的管理 (1)信号集(sigset_t) (2)block表的管理 ①操作相关的函数 ②sigpr…...

线上问题——频繁 Full GC 问题的排查思路

文章目录 一、查看 GC 日志二、分析内存泄漏三、检查对象生命周期四、优化代码五、调整垃圾回收策略六、使用监控工具 一、查看 GC 日志 启用 GC 日志 在 Java 应用中,需要在启动参数中添加适当的参数来启用 GC 日志记录。可以使用-XX:PrintGCDetails、-XX:PrintGCD…...

《探秘 Qt Creator Manual 4.11.1》

《探秘 Qt Creator Manual 4.11.1》 一、Qt Creator 4.11.1 概述二、功能特性全解析(一)跨平台能力展示(二)代码编辑优势(三)版本控制集成(四)特定 Qt 功能呈现(五&#…...

level2逐笔委托查询接口

沪深逐笔委托队列查询 前置步骤 分配数据库服务器 查询模板 以下是沪深委托队列查询的请求模板&#xff1a; http://<数据库服务器>/sql?modeorder_book&code<股票代码>&offset<offset>&token<token>查询参数说明 参数名类型说明mo…...

在Linux系统安装配置 MySQL 和 hive,hive配置为远程模式

前提&#xff1a;已安装配置好了Hadoop环境&#xff0c;因为hive的底层是Hadoop 1 Mysql安装 搜索Centos7自带的mariadb rpm -qa|grep mariadb 卸载mariadb rpm -e mariadb-libs-5.5.64-1.el7.x86_64 --nodeps 再搜索一次看看是否还存在 rpm -qa|grep mariadb 安装mysql 创…...

如何写好一份科技报告

如何写好一份技术文档 一、科技报告写作的整体框架封面与摘要&#xff1a;引言&#xff1a;理论框架与文献综述&#xff1a;实验方法与材料&#xff1a;实验结果&#xff1a;结果分析与讨论&#xff1a;结论&#xff1a;参考文献&#xff1a;附录&#xff1a; 二、科技报告写作…...

ARM学习(38)多进程多线程之间的通信方式

ARM学习(38)ARM学习(38)多进程多线程之间的通信方式 一、问题背景 笔者在调试模拟器的时候,碰到进程间通信的问题,一个进程在等另外一个进程ready的时候,迟迟等不到,然后通过调试发现,另外一个进程变量已经变化了,但是当前进程变量没变化,需要了解进程间通信的方式…...

《图解机器学习》(杉山将著)第一部分绪论学习笔记

《图解机器学习》&#xff08;杉山将著&#xff09;第一部分绪论学习笔记 《图解机器学习》&#xff08;杉山将著&#xff09;第一部分绪论学习笔记一、什么是机器学习1.1 学习的种类1.2 机器学习任务的例子1.3 机器学习的方法 二、学习模型2.1 线性模型2.2 核模型2.3 层级模型…...

【WPF】RenderTargetBitmap的使用

在WPF&#xff08;Windows Presentation Foundation&#xff09;中&#xff0c;RenderTargetBitmap 是一个非常有用的类&#xff0c;它允许你将任何可视元素&#xff08;如 UIElement 或 Visual 的实例&#xff09;渲染到位图中。这在需要生成图像快照、导出可视化内容为图片文…...

编辑, 抽成组件

问题 错误思路&#xff1a; 1 dept不能修改&#xff0c; 用watch监听一下&#xff1a;赋值给新的变量进行修改&#xff0c; 问题&#xff1a; currentDept 发生改变&#xff0c; depth也发生了改变&#xff0c;因为是浅拷贝&#xff0c; 用了json.pase(json.stringify(value…...

使用C#绘制具有平滑阴影颜色的曼德布洛特集分形

示例使用复数类在 C# 中轻松绘制曼德布洛特集分形解释了如何通过迭代方程绘制曼德布洛特集:...

【批量生成WORD和PDF文件】根据表格内容和模板文件批量创建word文件,一次性生成多个word文档和批量创建PDF文件

如何按照Word模板和表格的数据快速制作5000个word文档 &#xff1f; 在与客户的合作的中需要创建大量的合同&#xff0c;这些合同的模板大概都是一致的&#xff0c;是不是每次我们都需要填充不一样的数据来完成&#xff1f; 今天用表格数据完成合同模板的填充&#xff0c;批量…...

低延迟!实时处理!中软高科AI边缘服务器,解决边缘计算多样化需求!

根据相关统计&#xff0c;随着物联网的发展和5G技术的普及&#xff0c;到2025年&#xff0c;全球物联网设备连接数将达到1000亿&#xff0c;海量的计算数据使得传输到云端再处理的云计算方式显得更捉襟见肘。拥有低延迟、实时处理、可扩展性和更高安全性的边缘计算应运而生&…...

内旋风铣削知识再学习

最近被有不少小伙伴们问到蜗杆加工的一种方式——内旋风铣削加工。关于旋风铣之前出过一篇《什么是旋风铣&#xff1f;》&#xff0c;简要介绍了旋风铣&#xff08;Whilring&#xff09;的一些基本内容。本期再重新仔细聊一聊内旋风这种加工方式&#xff0c;可加工的零件种类&a…...

Redis 7.x如何安装与配置?保姆级教程

大家好&#xff0c;我是袁庭新。最新写了一套最新版的Redis 7.x企业级开发教程&#xff0c;今天先给大家介绍下Redis 7.x如何在Linux系统上安装和配置。 1 Redis下载与安装 使用非关系型数据库Redis必须先进行安装配置并开启Redis服务&#xff0c;然后使用对应客户端连接使用…...

SLAAC如何工作?

SLAAC如何工作&#xff1f; IPv6无状态地址自动配置(SLAAC)-常见问题 - 苍然满关中 - 博客园 https://support.huawei.com/enterprise/zh/doc/EDOC1100323788?sectionj00shttps://www.zhihu.com/question/6691553243/answer/57023796400 主机在启动或接口UP后&#xff0c;发…...

电脑丢失dll文件一键修复的多种方法分析,电脑故障修复攻略

电脑在使用过程中&#xff0c;有时会遇到DLL文件丢失的情况&#xff0c;这可能导致软件无法正常运行或系统出现故障。当面对这种状况时&#xff0c;不必过于慌张&#xff0c;因为有多种有效的修复方法可供选择。下面我们一起来看看电脑丢失dll文件的多种解决方法。 一.了解什么…...

Docker容器命令

docker 命令说明docker pull拉取镜像docker push推送镜像到DockerRegistrydocker images查看本地镜像docker rmi删除本地镜像docker run创建并运行容器&#xff08;不能重复创建&#xff09;docker stop停止指定容器docker start启动指定容器docker restart重新启动容器docker…...

【Leetcode 每日一题】3291. 形成目标字符串需要的最少字符串数 I

问题背景 给你一个字符串数组 w o r d s words words 和一个字符串 t a r g e t target target。 如果字符串 x x x 是 w o r d s words words 中 任意 字符串的 前缀&#xff08;字符串的前缀是从字符串的开头开始并延伸到其中任意点的子串&#xff09;&#xff0c;则认为…...

Windows聚焦壁纸代理不更新——解除UWP应用回环限制

开代理后经常出现Microsoft store打不开&#xff0c;聚焦壁纸不更新的情况&#xff0c;因为UWP应用默认禁止回环地址&#xff0c;导致开了代理以后不仅用不了代理上网&#xff0c;还把自己的本来的通信堵死了 打开CMD输入 FOR /F "tokens11 delims\" %p IN (REG QUER…...

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

web vue 项目 Docker化部署

Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段&#xff1a; 构建阶段&#xff08;Build Stage&#xff09;&#xff1a…...

C++初阶-list的底层

目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

【WiFi帧结构】

文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成&#xff1a;MAC头部frame bodyFCS&#xff0c;其中MAC是固定格式的&#xff0c;frame body是可变长度。 MAC头部有frame control&#xff0c;duration&#xff0c;address1&#xff0c;address2&#xff0c;addre…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别

UnsatisfiedLinkError 在对接硬件设备中&#xff0c;我们会遇到使用 java 调用 dll文件 的情况&#xff0c;此时大概率出现UnsatisfiedLinkError链接错误&#xff0c;原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用&#xff0c;结果 dll 未实现 JNI 协…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止

<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet&#xff1a; https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...

django filter 统计数量 按属性去重

在Django中&#xff0c;如果你想要根据某个属性对查询集进行去重并统计数量&#xff0c;你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求&#xff1a; 方法1&#xff1a;使用annotate()和Count 假设你有一个模型Item&#xff0c;并且你想…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

oracle与MySQL数据库之间数据同步的技术要点

Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异&#xff0c;它们的数据同步要求既要保持数据的准确性和一致性&#xff0c;又要处理好性能问题。以下是一些主要的技术要点&#xff1a; 数据结构差异 数据类型差异&#xff…...