【Linux系统】信号:信号保存 / 信号处理、内核态 / 用户态、操作系统运行原理(中断)

理解Linux系统内进程信号的整个流程可分为:
-
信号产生
-
信号保存
-
信号处理
上篇文章重点讲解了 信号的产生,本文会讲解信号的保存和信号处理相关的概念和操作:
两种信号默认处理
1、信号处理之忽略
::signal(2, SIG_IGN); // ignore: 忽略
#include <vector>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>void handler(int signo)
{std::cout << "get a new signal: " << signo << std::endl;exit(1);
}int main()
{// 信号捕捉:// 1. 默认// 2. 忽略// 3. 自定义捕捉::signal(2, SIG_IGN); // ignore: 忽略while(true){pause();}
}
运行结果如下: 显然对二号信号(ctrl+c) 没有效果了

2、信号处理之默认
::signal(2, SIG_DFL); // default:默认。
#include <vector>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <iostream>
#include <string>void handler(int signo)
{std::cout << "get a new signal: " << signo << std::endl;exit(1);
}int main()
{// 信号捕捉:// 1. 默认// 2. 忽略// 3. 自定义捕捉//::signal(2,SIG IGN);// ignore:忽略:本身就是一种信号捕捉的方法,动作是忽略::signal(2, SIG_DFL); // default:默认。while (true){pause();}
}
这些本质上是宏,而且是被强转后的

信号保存
1、信号保存相关概念
信号递达 / 信号未决 / 阻塞信号
-
实际执行信号的处理动作称为信号递达(Delivery)。
-
信号从产生到递达之间的状态,称为信号未决(Pending)。
-
进程可以选择阻塞(Block)某个信号。
-
被阻塞的信号产生时将保持在未决状态(Pending),直到进程解除对此信号的阻塞,才执行递达的动作。
-
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
简单来说:
-
信号递达:信号已经被接收处理了
-
信号未决:信号未被处理之前的状态
-
阻塞信号:可以使某个信号不能被处理,该信号会一直被保存为未处理之前的状态,即信号未决 pending 状态
这里的阻塞呢和进程进行 IO 获取数据的阻塞不一样,他们是完全不同的概念
这个阻塞是翻译 block 的问题
其实,信号未决(Pending) 叫做屏蔽信号会更加好理解
2、信号相关的三张表
block 表 / Pending 表 / handler表

Pending 表 的作用由图中可以看到,是一种位图结构的表,不过该位图不是只有一个整数,而是有系统自己封装的结构
handler表
handler_t XXX[N]:函数指针数组- 信号编号:就是函数指针数组的下标!
其中,该表内的前两项刚好是 0 和 1,也就是两个信号处理的宏定义:忽略和默认

该 handler表函数指针数组中的每个数组元素都是一个函数指针,每个指针都对应指向 该数组下标序号的信号 的默认信号处理方式,如 信号 2 ,即对应数组下标为 2,这个指针指向信号 2 的默认处理函数
我们使用系统调用 signal(2, handler) 就是通过信号 2 的编号索引对应 handler 表的位置(即数组下标为 2 的位置),修改对应的函数指针指向用户自定义的处理函数,这样就完成了自定义信号处理的定义
这就解释了,为什么 系统调用 signal(2, handler) 在整个程序全局中只需定义一次,因为函数指针数组 handler 表修改一次指向的函数即可
Block 表

Block 表 就是用来决定是否阻塞或屏蔽特定信号的!
这三个表的顺序就像图中所示:只要**Block 表**将某个信号屏蔽了,即使该信号已经在 pending 表 中,它也无法通过查找 handler 表 来执行相应的处理方法!
简单来说,如果你在 Block 表 中屏蔽了一个信号,即便之后进程接收到了这个信号,它也不会生效。
问题:我们能否提前屏蔽一个信号?这与当前是否已经接收到该信号有关系吗?
答:可以提前进行信号的屏蔽。因为只有当信号屏蔽设置好了,比信号实际到达要早,这样才能有效地阻止该信号生效。
到这里,这就回答了“你如何识别信号?”这个问题。
信号的识别是内建的功能。进程能够识别信号,是因为程序员在编写程序时内置了这一特性。通过使用这三张表(Block 表、Pending 表和Handler 表),就可以让进程具备识别和处理信号的能力。
3、三张表的内核源码
// 内核结构 2.6.18
struct task_struct {/* signal handlers */struct sighand_struct *sighand; // handler表指针sigset_t blocked; // block 表: 屏蔽信号表struct sigpending pending; // pending 表: 信号未决表
};// handler表结构:包含函数指针数组
struct sighand_struct {atomic_t count;struct k_sigaction action[_NSIG]; // #define _NSIG 64spinlock_t siglock;
};// handler表结构中的函数指针数组的元素的结构类型
struct k_sigaction {struct __new_sigaction sa; void __user *ka_restorer;
};/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);struct __new_sigaction {__sighandler_t sa_handler;unsigned long sa_flags;void (*sa_restorer)(void); /* Not used by Linux/SPARC */__new_sigset_t sa_mask;
};// pending 表 的结构类型
struct sigpending {struct list_head list;sigset_t signal;
};// sigset_t : 是系统封装的位图结构
typedef struct {unsigned long long sig[_NSIG_WORDS];
} sigset_t;
问题:为什么要对位图封装成结构体
答:利于扩展、利于该结构整体使用(定义对象就可以获取该位图)
4、sigset_t 信号集
从前面的图中可以看出,每个信号只有一个 bit 用于未决标志,非 0 即 1,这意味着它并不记录该信号产生了多少次。阻塞标志也是以同样的方式表示的。因此,未决状态和阻塞状态可以使用相同的数据类型 sigset_t 来存储。可以说 sigset_t 是一种信号集数据类型。
具体来说,在阻塞信号集中,“有效”和“无效”指的是该信号是否被阻塞;而在未决信号集中,“有效”和“无效”则表示该信号是否处于未决状态。
阻塞信号集也被称为当前进程的信号屏蔽字(Signal Mask)。
简而言之,你可以把这想象成一个32位整数的位图。每个位代表一个信号的状态,无论是未决还是阻塞状态,都通过设置相应的位来标记为“有效”或“无效”。
5、信号集操作函数
sigset_t 类型使用一个 bit 来表示每种信号的“有效”或“无效”状态。至于这个类型内部如何存储这些 bit,则依赖于系统的具体实现。从使用者的角度来看,这其实是不需要关心的细节。使用者应该仅通过调用特定的函数来操作 sigset_t 变量,而不应对它的内部数据进行任何直接解释或修改。例如,直接使用 printf 打印 sigset_t 变量是没有意义的。
简单来说:信号集 sigset_t 是系统封装好的一种类型,不建议用户自行使用位操作等手段对该“位图”进行操作。相反,应当使用系统提供的信号集操作函数来进行处理。
信号集操作函数就是对该 信号集 sigset_t 类型的增删查改
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空:全部置为0
int sigfillset(sigset_t *set); // 使满:全部置为1
int sigaddset(sigset_t *set, int signo); // 添加:向指定信号集,添加对应信号
int sigdelset(sigset_t *set, int signo); // 删除:向指定信号集,删除对应信号
int sigismember(const sigset_t *set, int signo);// 查找:在指定信号集,查找是否有该信号
注意:在使用 sigset_t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 进行初始化,以确保信号集处于一个确定的状态。初始化 sigset_t 变量之后,就可以通过调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
6、sigprocmask :修改进程的 block 表
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(即阻塞信号集)。
上一点讲解的各个信号集操作函数,是用于对一个信号集 sigset_t 类型的增删查改,而此处学习的 sigprocmask 则是修改本进程的 信号屏蔽字
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为 0,若出错则为 -1
- 如果
oset是非空指针,则通过oset参数读取并传出进程的当前信号屏蔽字(阻塞信号集)。 - 如果
set是非空指针,则更改进程的信号屏蔽字,参数how指示如何进行更改。具体来说: - 如果
oset和set都是非空指针,则首先将原来的信号屏蔽字备份到oset中,然后根据set和how参数来更改信号屏蔽字。
假设当前的信号屏蔽字为 mask,how 参数的可选值及其含义如下:
具体来说:
int how :传递操作选项

-
SIG_BLOCK:将set中设置的信号,添加到修改进程的block表(相当于添加对应信号) -
SIG_UNBLOCK:将set中设置的信号,解除进程的block表对应的信号(相当于删除对应信号) -
SIG_SETMASK:将set中设置的信号,直接设置成为进程的block表(相当于覆盖)
const sigset_t *set :传递设置期望的信号集
sigset_t *oset :输出型参数,就是 old set 将旧的信号集保存下来,因为后续可能还需用于恢复
简单来说:我们通过一系列信号集操作函数,设置一个我们期望的信号集,通过系统调用 sigprocmask 修改进程的 block 表
7、sigpending :读取当前进程的 pending 表
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过参数 set 传出
调⽤成功则返回 0 ,出错则返回 -1
该函数只是用于获取 pending 表,而系统不提供修改 pending 表 的函数接口,没必要,因为上一章节讲解的 5 种信号产生的方式都在修改 pending 表!
8、做实验:验证 block 表的效果
演示屏蔽 2 号信号

下面这段代码:
先使用 sigprocmask ,修改进程的 block 表,屏蔽 2 号信号
通过循环打印当前进程的 pending 表,然后通过另一个终端向该进程发送 2 号信号
#include <iostream>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
using namespace std;void PrintPending(sigset_t& pending)
{// 打印pending表的前32位信号:后面的信号是实时信号不打印// int sigismember(const sigset_t *set, int signo);// 若包含则返回1,不包含则返回0,出错返回-1cout << "pending: ";for(int i = 0; i < 32; ++i){int ret = sigismember(&pending, i);if(ret != -1) cout << ret << " ";}cout << '\n';
}int main()
{//(1)block表屏蔽2号信号//(2)不断打印pending表//(3)发送2号 ->看到2号信号的pending效果!/*int sigemptyset(sigset_t *set); // 清空:全部置为0int sigaddset(sigset_t *set, int signo); // 添加:向指定信号集,添加对应信号int sigdelset(sigset_t *set, int signo); // 删除:向指定信号集,删除对应信号*///设置存有2号信号的信号集sigset_t set, oset;sigemptyset(&set);sigaddset(&set, 2);// block表屏蔽2号信号sigprocmask(SIG_BLOCK, &set, &oset);int cnt = 0;while(true){// 不断打印pending表sigset_t pending;sigpending(&pending);PrintPending(pending);cnt++;sleep(1);}
}
运行结果如下:循环打印当前进程的 pending 表
当另一个终端向该进程发送 2 号信号时,当前进程的 pending 表的 第二个位置信号置为 1
证明了 2 号信号被 block 成功屏蔽!

演示去除对 2 号信号的屏蔽
循环中加入:当到达 cnt = 10 时,去除对 2 号信号的屏蔽
#include <iostream>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
using namespace std;void handler(int signo)
{std::cout << "get a new signal: " << signo << std::endl;//exit(1);
}void PrintPending(sigset_t& pending)
{// 打印pending表的前32位信号:后面的信号是实时信号不打印// int sigismember(const sigset_t *set, int signo);// 若包含则返回1,不包含则返回0,出错返回-1printf("pending [pid %d] : ", getpid());for(int i = 0; i < 32; ++i){int ret = sigismember(&pending, i);if(ret != -1) cout << ret << " ";}cout << '\n';
}int main()
{//(1)block表屏蔽2号信号//(2)不断打印pending表//(3)发送2号 ->看到2号信号的pending效果!/*int sigemptyset(sigset_t *set); // 清空:全部置为0int sigaddset(sigset_t *set, int signo); // 添加:向指定信号集,添加对应信号int sigdelset(sigset_t *set, int signo); // 删除:向指定信号集,删除对应信号*///设置存有2号信号的信号集sigset_t set, oset;sigemptyset(&set);sigaddset(&set, 2);// block表屏蔽2号信号sigprocmask(SIG_BLOCK, &set, &oset);// 给2号信号添加自定义处理函数:方便解除对2号信号的屏蔽时,可以查看pending表的变化,不至于因为2号信号杀掉进程导致进程退出signal(2, handler);int cnt = 0;while(true){// 不断打印pending表sigset_t pending;sigpending(&pending);PrintPending(pending);cnt++;sleep(1);if(cnt == 10){std::cout<<"解除对2号信号的屏蔽:"<<std::endl;// 将block表中2号信号的屏蔽消除:即旧的block表覆盖回去sigprocmask(SIG_SETMASK, &oset, NULL);}}
}
运行结果:

9、用户态和内核态(重要)
问题:信号来了,并不是立即处理的。什么时候处理?
答:当进程从内核态返回用户态时,会检查当前是否有未决(pending)且未被阻塞的信号。如果有,就会根据 handler 表来处理这些信号。
这些概念后文会详细讲解
9.1 何为用户态和内核态(浅显理解)

9.2 信号有自定义处理的情况

注意,上面这种情况会发生 4 次 用户态和内核态 的转变
这个无穷符号的中间交点在内核态里面
在执行主控制流程的某条指令时因为中断、异常或系统调用进入内核
进入内核后会回到用户态,回去之前会自动检测一下 pending 表和 block 表,查询是否有信号需要处理

类似于下面的流程:
对于信号的自定义处理或信号的默认处理,可以理解为独立于进程运行的程序之外

9.3 何为用户态和内核态(深度理解)
穿插话题 - 操作系统是怎么运行的
硬件中断:

这个操作系统的中断向量表可以看作一个函数指针数组:IDT[N],通过数组下标索引对应的中断处理服务”函数“,这个数组下标就是 中断号
执行中断例程:
1、保存现场
2、通过中断号n,查表
3、调用对应的中断方法
例如外设磁盘需要将部分数据写到内存,当磁盘准备好了,通过一个硬件中断,中断控制器通知 CPU,CPU得知并获取对应的中断号,通过该中断号索引中断向量表的对应中断处理服务,
操作系统通过该中断服务将磁盘的就绪的数据读入内存
- 中断向量表就是操作系统的⼀部分,启动就加载到内存中了,操作系统主函数中含有一个“硬件中断向量表初始化逻辑,如下源码展示:
tap_init(void)” - 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
- 由外部设备触发的,中断系统运行流程,叫做硬件中断
//Linux内核0.11源码
void trap_init(void)
{int i;set_trap_gate(0,÷_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,&irq13);// 设置协处理器的陷阱⻔。outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。set_trap_gate(39,¶llel_interrupt);// 设置并⾏⼝的陷阱⻔。
}void rs_init (void)
{set_intr_gate (0x24, rs1_interrupt); // 设置串⾏⼝1 的中断⻔向量(硬件IRQ4 信号)。set_intr_gate (0x23, rs2_interrupt); // 设置串⾏⼝2 的中断⻔向量(硬件IRQ3 信号)。init (tty_table[1].read_q.data); // 初始化串⾏⼝1(.data 是端⼝号)。init (tty_table[2].read_q.data); // 初始化串⾏⼝2。outb (inb_p (0x21) & 0xE7, 0x21); // 允许主8259A 芯⽚的IRQ3,IRQ4 中断信号请求。
}
时钟中断
问题:
- 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执⾏呢??
- 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?
如下图,会有一个硬件:时钟源,向CPU发送时钟中断,CPU根据该中断号执行时钟源对应的 中断服务:进程调度等操作

只要时钟源发送时钟中断,操作系统就会不断的进行进程调度等操作,这样不就通过
时钟中断,一直在推进操作系统进行调度!
什么是操作系统?操作系统就是基于中断向量表,进行工作的!!!
操作系统在时钟中断的推动下,不断的进行进程调度
因为时间源这个硬件需要不断按一定时间的发送时钟中断,现代机器的设计干脆直接将时间源集成到 CPU 内部,这就叫做主频!!!
主频的速度越快,发送的时钟中断的频率越高,操作系统内部处理进程调度进程的速度越快,一定程度上影响电脑性能,因此主频越高电脑一般越贵
时钟中断对应的中断处理服务不直接是进程调度,而是一个函数,该函数内部含有进程调度的相关处理逻辑:
我们看下源码

其中 schedule() 就是用于进程调度的函数,
这样,操作系统不就在硬件的推动下,自动调度了么
// Linux 内核0.11// main.c
sched_init(); // 调度程序初始化(加载了任务0 的tr, ldtr) (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_call.s
_timer_interrupt:
//...;// do_timer(CPL)执⾏任务切换、计时等⼯作,在kernel/shched.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 的任务,并运⾏之。
}
死循环
如果是这样,操作系统不就可以躺平了吗?对,操作系统⾃⼰不做任何事情,需要什么功能,就向中断向量表⾥⾯添加⽅法即可
操作系统的本质:就是⼀个死循环!循环进行 pause()
需要进程调度就通过时钟中断来告诉操作系统要干活了,否则就死循环的呆着!
void main(void) /* 这⾥确实是void,并没错。 */
{ /* 在startup 程序(head.s)中就是这样假设的。 *///.../** 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返* 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任* 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时),* 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没* 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。*/for (;;)pause();
}
// end main
因此 我们之前写的通过信号模拟实现操作系统的代码中,void Handler(int signum) 这个自定义信号处理函数,不就可以类似传入中断号,索引查询中断向量表,执行对应的中断处理函数吗??
这样操作系统只需要死循环等待着硬件发来中断,再干活,
因此操作系统也可以称为通过中断推动运行的进程
#include<iostream>
#include<functional>
#include<vector>
#include<unistd.h>
#include <signal.h>
using namespace std;// 定义一个函数指针类型,用于处理信号
typedef void (*sighandler_t)(int);
// 定义一个函数对象类型,用于存储要执行的函数
using func = function<void()>;
// 定义一个函数对象向量,用于存储多个要执行的函数
vector<func>funcV;
// 定义一个计数器变量
int count = 0;// 信号处理函数,当接收到信号时,执行向量中的所有函数
void Handler(int signum)
{// 遍历函数对象向量for(auto& f : funcV){// 执行每个函数f();}// 输出计数器的值和分割线cout << "—————————— count = " << count << "——————————" << '\n';// 设置一个新的闹钟,1 秒后触发alarm(1);
}int main()
{// 设置一个 1 秒后触发的闹钟alarm(1);// 注册信号处理函数,当接收到 SIGALRM 信号时,调用 Handler 函数signal(SIGALRM, Handler); // signal用于整个程序,只会捕获单个信号// 向函数对象向量中添加一些函数funcV.push_back([](){cout << "我是一个内核刷新操作" << '\n';});funcV.push_back([](){cout << "我是一个检测进程时间片的操作,如果时间片到了,我会切换进程" << '\n';});funcV.push_back([](){cout << "我是一个内存管理操作,定期清理操作系统内部的内存碎片" << '\n';});// 进入一个无限循环,程序不会退出while(1){pause();cout << "我醒来了~" << '\n';count++;}; // 死循环,不退出return 0;
}
时间片
进程调度时,每个被调度的进程都会被分配一个时间片,时间片实际上就是存储到进程PCB中的一个整型变量:int count
每次CPU内部的主频,即时钟源,发出一个时钟中断,操作系统处理时钟中断时,就会给当前调度的进程的时间片 :count--
当时间片减为零时,表示本轮该进程调度结束,此时就准备进程切换了

给当前调度的进程的时间片 :count--的逻辑就是在时钟中断对应的中断处理函数中的 do_timer()

进程相关切换逻辑好像就是放到 schedule() 函数中:

软中断
- 外部硬件中断:需要由硬件设备触发。
- 软件触发的中断(软中断):是的,可以通过软件原因触发类似的逻辑。为了让操作系统支持系统调用,CPU设计了相应的汇编指令(如
int或syscall),使得在没有外部硬件中断的情况下,通过这些指令也能触发中断逻辑。
这样通过软件实现上述逻辑的机制被称为软中断。软中断有固定的中断号,用来索引特定的中断处理程序,常见的形式包括 syscall: XXX 或 int: 0x80。
操作系统会在中断向量表中为软中断配置处理方法,并将系统调用的入口函数放置于此。当触发软中断时,会通过这个入口函数找到对应的系统调用函数指针数组,进而匹配并调用具体的系统调用。系统调用表使用系统调用号作为数组下标来查找对应的系统调用。
系统调用过程
系统调用的过程本质上是通过触发软中断(例如 int 0x80 或 syscall),使CPU执行该软中断对于的中断处理例程,该中断处理函数通常是系统调用操作函数的入口,通过该函数可以找到系统调用数组。接着,以系统调用号作为下标查询该系统调用数组,找到并执行对应的系统调用程序操作。
问题:如何让操作系统知道系统调用号?
操作系统通过CPU的一个寄存器(比如 EAX)获取系统调用号。不需要传递系统调用号作为参数,在系统调用处理方法 void sys_function() 中有一些汇编代码(如 move XXX eax),用于从寄存器中取出预先存储的系统调用号。
系统调用所需的相关参数也通过寄存器传递给操作系统。
问题:操作系统如何返回结果给用户?
操作系统通过寄存器或用户传入的缓冲区地址返回结果。例如,在汇编层面,callq func 调用某个函数之后,通常跟着一个 move 指令,用于将某个寄存器中的返回值写入指定变量。
因此,在底层操作系统的通信过程中,信息的传递一般通过寄存器完成。
我们看一下系统调用处理函数的源码::是使用汇编实现的

其中:这句指令就能说明操作系统如何查找系统调用表的

_sys_call_table_是系统调用表的开始指针地址eax寄存器中存储着系统调用号,即系统调用表数组下标eax*4:表示通过系统调用号*4 == 对应系统调用的地址(4 为当前系统的指针大小)
定位到 _sys_call_table_ 系统调用表:可以看到该表存储着大部分系统调用函数

因此,系统调用的调用流程是:
通过触发软中断进入内核,根据中断号找到系统调用入口函数。在寄存器中存放系统调用号,并通过一句汇编代码计算出该系统调用在系统调用表中的位置,从而找到并执行相应的系统调用。
实际上,我们上层使用的系统调用是经过封装的,系统调用的本质是 中断号(用于陷入内核)+汇编代码(临时存放传递进来的参数和接收返回值)+系统调用号(用于查询系统调用数组中的系统调用程序)
问题:用户自己可以设计用户层的系统调用吗?
我们是否可以认为,用户想调用操作系统中的系统调用,可以写一段这样的汇编代码,同时通过系统调用号计算出系统调用表中该系统调用的位置,然后找到并使用该系统调用?也就是说用户自己是否可以设计一个用户层的系统调用,用于调用系统内部的系统调用程序?
答:其实是可以的!
问题:但是为什么没见过有人这样用?
因为这样做过于麻烦。所以设计者将系统调用都封装成了函数,并集成到了 GNU glibc 库中。
在封装的系统调用内部:
- 拿到我们传递进来的参数。
- 使用设定好的固定系统调用号,通过汇编指令查表找到并执行对应的系统调用。
- 将返回值等信息存储在其他寄存器中,便于上层应用获取。
GNU glibc 库的作用
GNU glibc 库封装了各种平台的系统调用,使得用户可以更方便地使用这些功能,而不需要直接编写底层汇编代码。实际上,几乎所有的软件都或多或少与C语言有关联。
如何理解内核态和用户态
每个进程都有自己的虚拟地址空间,这个地址空间分为几个部分:
- 用户区:这部分地址空间是进程私有的,每个进程都有自己独立的一份用户区。用户区包含了进程的代码、数据、堆栈等。
- 内核区:这部分地址空间是所有进程共享的,包含了内核代码和数据结构。
用户页表和内核页表
-
用户页表:
- 每个进程都有自己独立的用户页表,用于映射用户区的虚拟地址到物理地址。
- 用户页表确保了每个进程的用户区是独立的,互不影响。
-
内核页表:
- 内核页表在整个操作系统中只有一份,所有进程共享这份内核页表,这样所有进程都能看到同一个操作系统(OS)。
- 内核页表用于映射内核区的虚拟地址到物理地址,确保所有进程都能访问相同的内核数据和代码。
内核页表的作用
-
共享内核数据:
- 内核页表使得所有进程都能看到同一个操作系统内核数据和代码,确保了内核功能的一致性和可靠性。
- 例如,内核数据结构如文件系统、网络协议栈等都是共享的。
-
增强进程独立性:
- 尽管内核页表是共享的,但每个进程的虚拟地址空间中都包含了一份内核页表的映射。
- 这样,进程在进行系统调用或其他内核操作时,可以直接在自己的虚拟地址空间中访问内核数据,而不需要切换到其他地址空间。
- 这种设计增强了进程的独立性,减少了上下文切换的开销。
简单总结
进程的虚拟地址空间分为两部分:用户区和内核区。用户区包括我们熟知的栈区、堆区、共享区、代码区、数据区等,是每个进程独有的。内核区则是独立的一个区域,用于存放操作系统内核的代码和数据。值得注意的是,内核区资源通常是只读不可修改的,整个操作系统只有一份内核页表,所有进程共享这份内核页表,从而所有进程都能看到同一个操作系统。当进程需要执行程序访问操作系统内核时,可以直接在自己的虚拟地址空间中的内核区访问,这使得操作更为便捷。
以设计者将系统调用都封装成了函数,并集成到了 GNU glibc 库中。
相关文章:
【Linux系统】信号:信号保存 / 信号处理、内核态 / 用户态、操作系统运行原理(中断)
理解Linux系统内进程信号的整个流程可分为: 信号产生 信号保存 信号处理 上篇文章重点讲解了 信号的产生,本文会讲解信号的保存和信号处理相关的概念和操作: 两种信号默认处理 1、信号处理之忽略 ::signal(2, SIG_IGN); // ignore: 忽略#…...
探索 Copilot:开启智能助手新时代
探索 Copilot:开启智能助手新时代 在当今数字化飞速发展的时代,人工智能(AI)正以前所未有的速度改变着我们的工作和生活方式。而 Copilot 作为一款强大的 AI 助手,凭借其多样的功能和高效的应用,正在成为众…...
解锁豆瓣高清海报(二) 使用 OpenCV 拼接和压缩
解锁豆瓣高清海报(二): 使用 OpenCV 拼接和压缩 脚本地址: 项目地址: Gazer PixelWeaver.py pixel_squeezer_cv2.py 前瞻 继上一篇“解锁豆瓣高清海报(一) 深度爬虫与requests进阶之路”成功爬取豆瓣电影海报之后,本文将介绍如何使用 OpenCV 对这些海报进行智…...
我用Ai学Android Jetpack Compose之Card
这篇学习一下Card。回答来自 通义千问。 我想学习Card,麻烦你介绍一下 当然可以!在 Jetpack Compose 中,Card 是一个非常常用的组件,用于创建带有阴影和圆角的卡片式布局。它可以帮助你轻松实现美观且一致的 UI 设计,…...
NLP深度学习 DAY4:Word2Vec详解:两种模式(CBOW与Skip-gram)
用稀疏向量表示文本,即所谓的词袋模型在 NLP 有着悠久的历史。正如上文中介绍的,早在 2001年就开始使用密集向量表示词或词嵌入。Mikolov等人在2013年提出的创新技术是通过去除隐藏层,逼近目标,进而使这些单词嵌入的训练更加高效。…...
论文阅读(十):用可分解图模型模拟连锁不平衡
1.论文链接:Modeling Linkage Disequilibrium with Decomposable Graphical Models 摘要: 本章介绍了使用可分解的图形模型(DGMs)表示遗传数据,或连锁不平衡(LD),各种下游应用程序之…...
Python中容器类型的数据(上)
若我们想将多个数据打包并且统一管理,应该怎么办? Python内置的数据类型如序列(列表、元组等)、集合和字典等可以容纳多项数据,我们称它们为容器类型的数据。 序列 序列 (sequence) 是一种可迭代的、元素有序的容器类型的数据。 序列包括列表 (list)…...
PySPARK带多组参数和标签的SparkSQL批量数据导出到S3的程序
设计一个基于多个带标签SparkSQL模板作为配置文件和多组参数的PySPARK代码程序,实现根据不同的输入参数自动批量地将数据导出为Parquet、CSV和Excel文件到S3上,标签和多个参数(以“_”分割)为组成导出数据文件名,文件已…...
蓝桥杯备考:模拟算法之字符串展开
P1098 [NOIP 2007 提高组] 字符串的展开 - 洛谷 | 计算机科学教育新生态 #include <iostream> #include <cctype> #include <algorithm> using namespace std; int p1,p2,p3; string s,ret; void add(char left,char right) {string tmp;for(char ch left1;…...
使用LLaMA-Factory对AI进行认知的微调
使用LLaMA-Factory对AI进行认知的微调 引言1. 安装LLaMA-Factory1.1. 克隆仓库1.2. 创建虚拟环境1.3. 安装LLaMA-Factory1.4. 验证 2. 准备数据2.1. 创建数据集2.2. 更新数据集信息 3. 启动LLaMA-Factory4. 进行微调4.1. 设置模型4.2. 预览数据集4.3. 设置学习率等参数4.4. 预览…...
@Nullable 注解
文章目录 解释 Nullable 注解注解的组成部分:如何使用 Nullable 注解a. 标注方法返回值:b. 标注方法参数:c. 标注字段: 结合其他工具与 Nonnull 配合使用总结 Nullable 注解在 Java 中的使用场景通常与 Nullability(空…...
Arduino大师练成手册 -- 控制 AS608 指纹识别模块
要在 Arduino 上控制 AS608 指纹识别模块,你可以按照以下步骤进行: 硬件连接 连接指纹模块:将 AS608 指纹模块与 Arduino 连接。通常,AS608 使用 UART 接口进行通信。你需要将 AS608 的 TX、RX、VCC 和 GND 引脚分别连接到 Ardu…...
Mask R-CNN与YOLOv8的区别
Mask R-CNN与YOLOv8虽然都是深度学习在计算机视觉领域的应用,但它们属于不同类型的视觉框架,各有特点和优势。 以下是关于 Mask R-CNN 和 YOLOv8 的详细对比分析,涵盖核心原理、性能差异、应用场景和选择建议: 1. 核心原理与功能…...
在Ubuntu上使用Docker部署DeepSeek
在Ubuntu上使用Docker部署DeepSeek,并确保其可以访问公网网址进行对话,可以按照以下步骤进行: 一、安装Docker 更新Ubuntu的软件包索引: sudo apt-get update安装必要的软件包,这些软件包允许apt通过HTTPS使用存储库…...
MySQL的覆盖索引
MySQL的覆盖索引 前言 当一个索引包含了查询所需的全部字段时,就可以提高查询效率,这样的索引又被称之为覆盖索引。 以MySQL常见的三种存储引擎为例:InnoDB、MyISAM、Memory,对于覆盖索引提高查询效率的方式均不同,…...
【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.12 连续数组:为什么contiguous这么重要?
2.12 连续数组:为什么contiguous这么重要? 目录 #mermaid-svg-wxhozKbHdFIldAkj {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-wxhozKbHdFIldAkj .error-icon{fill:#552222;}#mermaid-svg-…...
在React中使用redux
一、首先安装两个插件 1.Redux Toolkit 2.react-redux 第一步:创建模块counterStore 第二步:在store的入口文件进行子模块的导入组合 第三步:在index.js中进行store的全局注入 第四步:在组件中进行使用 第五步:在组件中…...
lstm预测
import numpy as np import pandas as pd import tensorflow as tf import math import matplotlib.pyplot as plt from sklearn.preprocessing import MinMaxScaler from keras.layers import LSTM,Activation,Dense,Dropout# 时间序列数据转换为监督学习的格式 def creatXY(d…...
《 C++ 点滴漫谈: 二十五 》空指针,隐秘而危险的杀手:程序崩溃的真凶就在你眼前!
摘要 本博客全面解析了 C 中指针与空值的相关知识,从基础概念到现代 C 的改进展开,涵盖了空指针的定义、表示方式、使用场景以及常见注意事项。同时,深入探讨了 nullptr 的引入及智能指针在提升代码安全性和简化内存管理方面的优势。通过实际…...
【AI】探索自然语言处理(NLP):从基础到前沿技术及代码实践
Hi ! 云边有个稻草人-CSDN博客 必须有为成功付出代价的决心,然后想办法付出这个代价。 目录 引言 1. 什么是自然语言处理(NLP)? 2. NLP的基础技术 2.1 词袋模型(Bag-of-Words,BoWÿ…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...
cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...
【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
C++.OpenGL (20/64)混合(Blending)
混合(Blending) 透明效果核心原理 #mermaid-svg-SWG0UzVfJms7Sm3e {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-icon{fill:#552222;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-text{fill…...
【C++特殊工具与技术】优化内存分配(一):C++中的内存分配
目录 一、C 内存的基本概念 1.1 内存的物理与逻辑结构 1.2 C 程序的内存区域划分 二、栈内存分配 2.1 栈内存的特点 2.2 栈内存分配示例 三、堆内存分配 3.1 new和delete操作符 4.2 内存泄漏与悬空指针问题 4.3 new和delete的重载 四、智能指针…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...
