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

Linux学习笔记】信号的产生和用户态和内核态

【Linux学习笔记】信号的产生和用户态和内核态

🔥个人主页大白的编程日记

🔥专栏Linux学习笔记
在这里插入图片描述


文章目录

  • 【Linux学习笔记】信号的产生和用户态和内核态
    • 前言
  • 1.保存信号
    • 1.1信号其他相关常见概念
    • 1.2在内核中的表示
    • 1.3 sigset_t
    • 1.4信号集操作函数
      • 1.4.1 sigprocmask
      • 1.4.2 sigpending
  • 2.捕捉信号
    • 2.1信号捕捉的流程
    • 2.2 硬件中断
      • 2.2.1 硬件中断
      • 2.3.2时钟中断
      • 2.3.3 循环
      • 2.3.4 软中断
      • 2.3.5 缺页中断:内存碎片处理?除零/野指针错误?
    • 2.4 加何理解内核态和用户态
  • 3. 可重入函数
  • 4. volatile
  • 5. SIGCHLD信号 - 选学了解
    • 后言

前言

哈喽,各位小伙伴大家好!上期我们讲了Linux基本指令及其分析(一) 今天我们讲的是Linux基本指令及其分析(二)。话不多说,我们进入正题!向大厂冲锋!
在这里插入图片描述

1.保存信号

当前阶段

在这里插入图片描述

1.1信号其他相关常见概念

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

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

1.2在内核中的表示

信号在内核中的表示示意图

在这里插入图片描述

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。
Linux是这样实现的:

  • 常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。。
1 // 内核结构 2.6.18   
2 struct task_struct {   
3   
4 /\* signal handlers \*/   
5 struct sighand_struct \*sighand;   
6 sigset_t blocked   
7 struct sigpending pending;   
8   
9 }   
10   
11 struct sighand_struct {   
12 atomic_t count;   
13 struct k_Sigaction action[_NSIG]// #define _NSIG 64   
14 spinlock_t siglock;   
15 }16   
17 struct __new_sigaction {   
18 __sighandler_t sa_handler;   
19 unsigned long sa_flags;   
20 void (\*sa_restorer)(void)/\* Not used by Linux/SPARC \*/   
21 _new_sigset_t sa_mask;   
22 }23   
24 struct k_sigaction   
25 struct __new_sigaction sa;   
26 void _user \*ka_restorer;   
27 }28 
29 /\* Type of a signal handler.   
30 typedef void (\*__sighandler_t)(int)   
31   
32 struct sigpending   
33 struct list_head list;   
34 sigset_t signal;   
35 }

1.3 sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,

  • 这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,
  • 而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
  • 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

1.4信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

1 #include <signal.h>   
2 int sigemptyset(sigset_t \*set);   
3 int sigfillset(sigset_t \*set);   
4 int Sigaddset(sigset_t \*set, int signo);   
5 int sigdelset(sigset_t \*set, int signo);   
6 int sigismember(const sigset_t \*set, int signo);

.函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。函数sigfilset初始化set所指向的信号集,使其中所有信号的对应bit置位,

表示该信号集的有效信号包括系统支持的所有信号。

. 注意,在使用sigset_t类型的变量之前,一定要调 用sigemptysetsigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

1.4.1 sigprocmask

调用函数 sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

1 #include <signal.h>int 
sigprocmask(int how, const sigset_t \*set, sigset_t \*oset);  
3 返回值:若成功则为0,若出错则为-1
  • 如果set是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
  • 如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。
  • 如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当 于mask=mask|set
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当 于mask=mask&~set
SIG_SETMAsK设置当前信号屏蔽字为set所指向的值,相当于mask=set

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

1.4.2 sigpending

1 #include <signal.h>  
2 int sigpending(sigset_t \*set);  
3  
4 读取当前进程的未决信号集,通过set参数传出。  
5 调用成功则返回0,出错则返回-1

下面用刚学的几个函数做个实验。程序如下:

1 #include <iostream>  
2 #include <unistd.h>  
3 #include <cstdio>  
4 #include <sys/types.h>  
5 #include <sys/wait.h>  
6  
7 void PrintPending(sigset_t &pending)  
8{  
9 $< <$   
10 $=$ $> = ~ 1$   
11 {  
12  
13 {  
14 std::cout << 1  
15 }  
16 else  
17 {  
18  
19 }  
20 }  
21 std::cout <<  
22 }  
23  
24 void handler(int signo)  
25{  
26 std::cout << signo << " 号信号被递达!!!" << std::endl;  
27  
28 std::cout << - - << std::endl;  
29 sigset_t pending;  
30 sigpending(&pending);  
31 PrintPending(pending);  
32 std::cout << - << std::endl;  
33}  
34  
35 int main()  
36{  
37 //0。捕捉2号信号  
38 signal(2,handler)// 自定义捕捉  
39 //signal(2,SIG_IGN);// 忽略-个信号  
40 //signal(2,SIG_DFL);// 信号的默认处理动作41   
42 //1。屏蔽2号信号   
43 sigset_t block_set,old_set;   
44 sigemptyset(&block_set);   
45 sigemptyset(&old_set);   
46 sigaddset(&block_set,SIGINT)// 我们有没有修改当前进行的内核   
47 //1.1设置进入进程的Block表中   
48 sigprocmask(SIG_BL0cK,&block_set,&old_set)// 真正的修改当前进行的内核block   
表,完成了对2号信号的屏蔽!   
49   
50 int cnt = 15;   
51 while (true)   
52 {   
53 //2。获取当前进程的pending信号集   
54 sigset_t pending;   
55 sigpending(&pending) ;   
56   
57 //3。打印pending信号集   
58 PrintPending(pending);   
59 cnt--;   
60   
61 //4。解除对2号信号白   
62 if (cnt $\begin{array} { r l } { \mathbf { \omega } = } & { { } \odot } \end{array}$ )   
63 {   
64   
65 sigprocmask(SIG_SETMASK, &old_set, &block_set);   
66 }   
67   
68 sleep(1);   
69 }   
70}   
71 \$ ./run   
72 curr process[448336]pending: 000000000000000000000000000000   
73 curr pr0cess[448336]pe0nding: 0000000000000000000000000   
74 ^Ccurr process[448336]pending: 0000000000000000000000000000010   
75 curr process[448336]pending: 0000000000000000000000000000010   
76 cur pr0ocess[448336]pending: 000000000000000000000000010   
77 cur process[448336]pending: 000000000000000000000000010   
78 curr pr0ocess[448336]pending: 00000000000000000000000010   
79 cur pr0cess[448336]pending: 000000000000000000000000010   
80 curr process[448336]pending: 0000000000000000000000000000010

程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会使SIGINT信号处于未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

2.捕捉信号

当前阶段

在这里插入图片描述

2.1信号捕捉的流程

在这里插入图片描述

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较较复杂,举例如下:

  • 用户程序注册了 SIGQUIT 信号的处理函数 sighandler。
  • 当前正在执行 main 函数,这时发生中断或异常切换到内核态。
  • 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达。
  • 内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数, sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
  • sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。
  • 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。

在这里插入图片描述
在这里插入图片描述

2.2 硬件中断

2.2.1 硬件中断

  • 中断向量表就是操作系统的一部分,启动就加载到内存中了
  • 通过外部硬件中断,操作系统不需要对外设进行任何周期性的检测或者轮询
  • 由外部设备触发的,中断系统运行流程,叫做硬件中断
// Linux内核0.11源码
void trap_init(void)
{int i;set_trap_gate(0,&divide_error); // 设置除操作出错的中断向量值。以下雷同。set_trap_gate(1,&debug);set_trap_gate(2,&nmi);set_system_gate(3,&int3);  /* int3-5 can be called from all */set_system_gate(4,&overflow);set_system_gate(5,&bounds);set_trap_gate(6,&invalid_op);set_trap_gate(7,&device_not_available);set_trap_gate(8,&double_fault);set_trap_gate(9,&coprocessor_segment_overrun);set_trap_gate(10,&invalid_TSS);set_trap_gate(11,&segment_not_present);set_trap_gate(12,&stack_segment);set_trap_gate(13,&general_protection);set_trap_gate(14,&page_fault);set_trap_gate(15,&reserved);set_trap_gate(16,&coprocessor_error);// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门for (i=17;i&lt;48;i++)set_trap_gate(i,&reserved);set_trap_gate(45,&irq); // 设置协处理器的陷阱门。outb_p(inb_p(0x21)&0xfb,0x21); // 允许主8259A 芯片的IRQ2 中断请求。outb(inb_p(0xA1)&0xdf,0xA1); // 允许从8259A 芯片的IRQ13 中断请求。set_trap_gate(39,&parallel_interrupt); // 设置并行口的陷阱门。
}void rs_init (void)
{set_intr_gate (0x24, rs1_interrupt); // 设置串行口1 的中断门向量(硬件IRQ3 值)。set_intr_gate (0x23, rs2_interrupt); // 设置串行口2 的中断门向量(硬件IRQ3 值)int (*tty_table[1]) (read_q.data); // 初始化串行口1 (.data 是端口号)。init (tty_table[0] = rs_init);outb (0x21 & mcr_t, 0x21); // 允许主8259A 芯片的IRQ3 , IRQ4 中断信号请求
}
  • 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
  • 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?

2.3.2时钟中断

问题: 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?.
外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?

// Linux内核0.11
sched_init(); // 调度程序初始化(闭断了任务0 的tr, ldt)。(kernel/sched.c)
void sched_init(void)
{set_intr_gate(0x20, timer_interrupt);// 修改中断控制器屏蔽码,允许时钟中断。outb(inb_p(0x21) & ~0x01, 0x21);// 设置系统调用中断门。set_system_gate(0x80, &system_call);
}
// system_interrupt.s
_timer_interrupt:...// do_timer(CPL)执行任务切换、计时等工作,在kernel/sched.c,305 行实现。call do_timer; // 'do_timer(long CPL)' does everything from// 调度入口void do_timer(long cpl){...schedule();void schedule(void){...switch_to(next); // 切换到任务号为next 的任务,并运行之。}}

在这里插入图片描述

2.3.3 循环

如果是这样,操作系统不就可以躺平了吗?对,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可。

  • 操作系统的本质:就是一个死循环!
void main(void) /* 这里确实是void,并没错。 */
{...* 注意!!对于任何其它的函数,'pause()'将意味着我们必须等待收到一个信号才会返回* 回到被运行状态,但任务0 (task0),是唯一的系统情况(参见'schedule()'),因为任*0 在任何空闲时间里都会被激活(当没有其它任务在运行时),* 因此对于任务0'pause()'仅仅意味着我们返回来查看是否有其它任务可以运行,如果没有* 有的话我们就回到这里,一直循环执行'pause()'*/for (;;)pause();
}

在这里插入图片描述

在这里插入图片描述

2.3.4 软中断

  • 上述外部硬件中断,需要硬件设备触发。
  • 有没有可能,因为软件原因,也触发上面的逻辑?有!
  • 为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内部触发中断逻辑。

所以:
在这里插入图片描述

问题:

  • 用户层怎么把系统调用号给操作系统? - 寄存器(比如EAX)

  • 操作系统怎么把返回值给用户? - 寄存器或者用户传入的缓冲区地址

  • 系统调用的过程,其实就是先int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法

  • 系统调用号的本质:数组下标!

  • 这样,操作系统,就可以在硬件时钟的推动下,自动调度了。

  • 所以,什么是时间片?CPU为什么会有主频?为什么主频越快,CPU越快?

  • 可是为什么我们用的系统调用,从来没有见过什么 int 0x80 或者 syscall 呢?都是直接调用上层的函数的啊?

  • 那是因为Linux的gnu C标准库,给我们把几乎所有的系统调用全部封装了。
    在这里插入图片描述

2.3.5 缺页中断:内存碎片处理?除零/野指针错误?

void trap_init(void)
{int i;set_trap_gate(0,&divide_error); // 设置除操作出错的中断向量值。以下雷同。set_trap_gate(1,&debug);set_trap_gate(2,&nmi);set_system_gate(3,&int3);  /* int3-5 can be called from all */set_system_gate(4,&overflow);set_system_gate(5,&bounds);set_trap_gate(6,&invalid_op);set_trap_gate(7,&device_not_available);set_trap_gate(8,&double_fault);set_trap_gate(9,&coprocessor_segment_overrun);set_trap_gate(10,&invalid_TSS);set_trap_gate(11,&segment_not_present);set_trap_gate(12,&stack_segment);set_trap_gate(13,&general_protection);set_trap_gate(14,&page_fault);set_trap_gate(15,&reserved);set_trap_gate(16,&coprocessor_error);// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。for (i=17;i<48;i++)set_trap_gate(i,&reserved);set_trap_gate(45,&irq); // 设置协处理器的陷阱门。outb_p(inb_p(0x21)&0xfb,0x21); // 允许主8259A 芯片的IRQ2 中断请求。outb(inb_p(0xA1)&0xdf,0xA1); // 允许从8259A 芯片的IRQ13 中断请求。set_trap_gate(39,&parallel_interrupt); // 设置并行口的陷阱门。
}
  • 缺页?中断内存碎片处理?除零/野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进程发送信号,杀掉进程等等。

🍓 所以:

  • 操作系统就是躺在中断处理例程上的代码块!
  • CPU内部的软中断,比如int 0x80或者syscall,我们叫做陷阱
  • CPU内部的软中断,比如除零/野指针等,我们叫做异常。(所以,能理解“缺页异常”为什么这么叫了吗?)

2.4 加何理解内核态和用户态

结论:

  • 操作系统无论怎么切换进程,都能找到同一个操作系统!换句话说就是操作系统系统调用方法的执行,是在进程的地址空间中执行的!

关于特权级别,涉及到段,段描述符,段选择子,DPL,CPL,RPL等概念,而现在芯片为了保证兼容性,已经非常复杂了,进而导致OS也必须得照顾它的复杂性,这块我们不做深究了。

  • 用户态就是执行用户[0,3]GB时所处的状态
  • 内核态就是执行内核[3,4]GB时所处的状态。
  • 区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。
  • 一般执行 int 0x80 或者 syscall 软中断,CPL会在核心态后自动变更
  • 这样会不会不安全?
    在这里插入图片描述

3. 可重入函数

在这里插入图片描述

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核态,再次回到用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态继续从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

在这里插入图片描述

4. volatile

该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下

[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>volatile int flag = 0;
void handler(int sig)
{printf("change flag 0 to 1\n");flag = 1;
}int main()
{signal(2, handler);while(1){printf("process quit normal\n");return 0;}
}
[hb@localhost code_test]$ cat Makefile
sig:sig.cgcc -o sig sig.c -O2
.PHONY:clean
clean:rm -f sig
[hb@localhost code_test]$ .(sig
^Cchange flag 0 to 1
^Cchange flag 0 to 1
  • volatile 作用:保持内存的实时性,告知编译器,该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

在这里插入图片描述

5. SIGCHLD信号 - 选学了解

  • 第一章讲过用wait和waitpid函数来管理子进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用waitpid清理子进程。
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>volatile int flag = 0;
void handler(int sig)
{printf("change flag 0 to 1\n");flag = 1;
}int main()
{signal(2, handler);while(1){if(flag){printf("process quit normal\n");return 0;}}
}
[hb@localhost code_test]$ cat Makefile
sig:sig.cgcc -o sig sig.c -O2
.PHONY:clean
clean:rm -f sig
[hb@localhost code_test]$ .(sig
^Cchange flag 0 to 1
^Cchange flag 0 to 1

在这里插入图片描述

后言

这就是信号的产生和用户态和内核态。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~


文章转载自:

http://OAZ7skiW.kpnpd.cn
http://9yqTF7el.kpnpd.cn
http://rmGvbcLS.kpnpd.cn
http://6OK23RsR.kpnpd.cn
http://fagioHfg.kpnpd.cn
http://7P9ekQ3Z.kpnpd.cn
http://LQnZnV8y.kpnpd.cn
http://Cu3bmIeO.kpnpd.cn
http://xpEUrW3m.kpnpd.cn
http://JFjabJ2D.kpnpd.cn
http://T6rbXVdp.kpnpd.cn
http://xKqxS41n.kpnpd.cn
http://O4nEq2Fx.kpnpd.cn
http://LtsgcJiY.kpnpd.cn
http://WBOc4Jb5.kpnpd.cn
http://VTGuoXpE.kpnpd.cn
http://sEy9MNTC.kpnpd.cn
http://aTAdEaz8.kpnpd.cn
http://3upQ2oIH.kpnpd.cn
http://KjL4tgWS.kpnpd.cn
http://DFqNexgw.kpnpd.cn
http://C5glmymW.kpnpd.cn
http://M9y0Z3rW.kpnpd.cn
http://xYBKFQ0P.kpnpd.cn
http://1Fx8ILzI.kpnpd.cn
http://BtPlE0R5.kpnpd.cn
http://mo86jXJl.kpnpd.cn
http://9zFDOLAB.kpnpd.cn
http://k9mZwXao.kpnpd.cn
http://YoGOdgUa.kpnpd.cn
http://www.dtcms.com/a/376100.html

相关文章:

  • SpringMvc常见问题
  • 在 CentOS 系统上实现定时执行 Python 邮件发送任务
  • 认知语义学对人工智能自然语言处理的影响与启示
  • 基于「YOLO目标检测 + 多模态AI分析」的植物病害检测分析系统(vue+flask+数据集+模型训练)
  • Chaos Mesh / LitmusChaos 混沌工程:验证 ABP 的韧性策略
  • 《C++ 基础进阶:内存开辟规则、类型转换原理与 IO 流高效使用》
  • AI在人力资源场景中的落地
  • 动态规划篇(背包问题)
  • 线程亲和性(Thread Affinity)
  • 三层交换机实现vlan互通
  • 【项目】在AUTODL上使用langchain实现《红楼梦》知识图谱和RAG混合检索(三)知识图谱和路由部分
  • MyBatis基础到高级实践:全方位指南(上)
  • 开始 ComfyUI 的 AI 绘图之旅-RealESRGAN图生图之图像放大(四)
  • [HUBUCTF 2022 新生赛]help
  • Matlab机器人工具箱6.1 导入stl模型——用SerialLink描述
  • 大数据存储域——Kafka设计原理
  • B站 韩顺平 笔记 (Day 28)
  • Biomedical HPC+AI Platform:48款计算生物学工具集成的一站式高性能在线平台,赋能药物发现
  • Linux 基础 IO 核心知识总结:从系统调用到缓冲区机制(一)
  • 滴滴二面(准备二)
  • leetcode14(判断子序列)
  • 深度学习基本模块:Conv2D 二维卷积层
  • spring中case一直返回else中的值-问题和原理详解
  • 传输层:UDP/TCP协议
  • Java学习之——“IO流“的进阶流之序列化流的学习
  • LeetCode 面试经典 150 题:轮转数组(三次翻转法详解 + 多解法对比)
  • 什么是PFC控制器
  • 【卷积神经网络详解与实例3】——池化与反池化操作
  • Bean的生命周期 高频考点!
  • Redis 主从复制详解:原理、配置与主从切换实战