【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: 忽略#…...
在C语言多线程环境中使用互斥量
如果有十个银行账号通过不同的十条线程同时向同一个账号转账时,如果没有很好的机制保证十个账号依次存入,那么这些转账可能出问题。我们可以通过互斥量来解决。 C标准库提供了这个互斥量,只需要引入threads.头文件。 互斥量就像是一把锁&am…...
PHP代码审计学习02
目录 代码审计一般思路 Beescms代码审计(upload) Finecms基于前台MVC任意文件上传挖掘思路 CLTPHP基于thinkphp5框架的文件上传挖掘思路 今天来看PHP有框架MVC类,文件上传,断点调试挖掘。 同样还是有关键字搜索和功能点抓包两…...
基于微信小程序的医院预约挂号系统设计与实现(LW+源码+讲解)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...
大厂面试题备份20250201
20250201 面试策略 如果三面往后遇到传说中让人忍受不了的业余面试官,就舔着苟过去,入职大概率见不着他,但一二面遇到,反问环节就主动说不够match,让释放流程。 机器/深度学习 百面机器学习 5.4 通用CS 计算机网…...
Spring Boot 实例解析:HelloWorld 探究
POM 文件剖析: 父项目: <parent><groupId>org.springframework.boot</groupId><artifactId>spring‐boot‐starter‐parent</artifactId><version>1.5.9.RELEASE</version> </parent> 他的父项目是 <…...
【课题推荐】基于t分布的非高斯滤波框架在水下自主导航中的应用研究
水下自主导航系统在海洋探测、环境监测及水下作业等领域具有广泛的应用。然而,复杂的水下环境常常导致传感器输出出现野值噪声,这些噪声会严重影响导航信息融合算法的精度,甚至导致系统发散。传统的卡尔曼滤波算法基于高斯噪声假设࿰…...
【C++语言】卡码网语言基础课系列----12. 位置互换
文章目录 练习题目位置互换具体代码实现 小白寄语诗词共勉 练习题目 位置互换 题目描述: 给定一个长度为偶数位的字符串,请编程实现字符串的奇偶位互换。 输入描述: 输入包含多组测试数据。 输入的第一行是一个整数n,表示有测试…...
洛谷的更多功能(不会像其他文章那样复杂且仅支持Edge浏览器)
第一步:下载《洛谷美化 (1).zip》文件夹。 会出现这样的文件夹: 注意:Edge.txt和洛谷前提1.txt是一样的哟! 第二步:篡改猴 先打开Edge.txt或者是洛谷前提1.txt文件,打开后复制粘贴到你的Edge浏览器并打开…...
C++编程语言:抽象机制:模板(Bjarne Stroustrup)
目录 23.1 引言和概观(Introduction and Overview) 23.2 一个简单的字符串模板(A Simple String Template) 23.2.1 模板的定义(Defining a Template) 23.2.2 模板实例化(Template Instantiation) 23.3 类型检查(Type Checking) 23.3.1 类型等价(Type Equivalence) …...
女生年薪12万,算不算属于高收入人群
在繁华喧嚣的都市中,我们时常会听到关于收入、高薪与生活质量等话题的讨论。尤其是对于年轻女性而言,薪资水平不仅关乎个人价值的体现,更直接影响到生活质量与未来的规划。那么,女生年薪12万,是否可以被划入高收入人群…...
2181、合并零之间的节点
2181、[中等] 合并零之间的节点 1、问题描述: 给你一个链表的头节点 head ,该链表包含由 0 分隔开的一连串整数。链表的 开端 和 末尾 的节点都满足 Node.val 0 。 对于每两个相邻的 0 ,请你将它们之间的所有节点合并成一个节点ÿ…...
Immutable设计 SimpleDateFormat DateTimeFormatter
专栏系列文章地址:https://blog.csdn.net/qq_26437925/article/details/145290162 本文目标: 理解不可变设计模式,时间format有线程安全要求的注意使用DateTimeFormatter 目录 ImmutableSimpleDateFormat 非线程安全可以synchronized解决&a…...
【网络】传输层协议TCP(重点)
文章目录 1. TCP协议段格式2. 详解TCP2.1 4位首部长度2.2 32位序号与32位确认序号(确认应答机制)2.3 超时重传机制2.4 连接管理机制(3次握手、4次挥手 3个标志位)2.5 16位窗口大小(流量控制)2.6 滑动窗口2.7 3个标志位 16位紧急…...
17.[前端开发]Day17-形变-动画-vertical-align
1 transform CSS属性 - transform transform的用法 表示一个或者多个 不用记住全部的函数,只用掌握这四个常用的函数即可 位移 - translate <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta ht…...
LeetCode435周赛T2贪心
题目描述 给你一个由字符 N、S、E 和 W 组成的字符串 s,其中 s[i] 表示在无限网格中的移动操作: N:向北移动 1 个单位。S:向南移动 1 个单位。E:向东移动 1 个单位。W:向西移动 1 个单位。 初始时&#…...
陆游的《诗人苦学说》:从藻绘到“功夫在诗外”(中英双语)mastery lies beyond poetry
陆游的《诗人苦学说》:从藻绘到“功夫在诗外” 今天看万维钢的《万万没想到》一书,看到陆游的功夫在诗外的句子,特意去查找这首诗的原文。故而有此文。 我国学人还往往过分强调“功夫在诗外”这句陆游的名言,认为提升综合素质是一…...
AI模型平台之——ModelScope(魔搭)
ModelScope 是什么? ModelScope 是一个由阿里巴巴达摩院推出的开源模型库和工具集,旨在为开发者提供高效、便捷的机器学习模型和工具。ModelScope 提供了丰富的预训练模型、数据集和工具,支持多种任务和应用场景,如自然语言处理、…...
GIt使用笔记大全
Git 使用笔记大全 1. 安装 Git 在终端或命令提示符中,输入以下命令检查是否已安装 Git: git --version如果未安装,可以从 Git 官方网站 下载并安装适合你操作系统的版本。 2. 配置 Git 首次使用 Git 时,需要配置用户名和邮箱…...
42【文件名的编码规则】
我们在学习的过程中,写出数据或读取数据时需要考虑编码类型 火山采用:UTF-16 易语言采用:GBK php采用:UTF-8 那么我们写出的文件名应该是何种编码的?比如火山程序向本地写出一个“测试.txt”,理论上这个“测…...
Linux网络 HTTPS 协议原理
概念 HTTPS 也是一个应用层协议,不过 是在 HTTP 协议的基础上引入了一个加密层。因为 HTTP的内容是明文传输的,明文数据会经过路由器、wifi 热点、通信服务运营商、代理服务器等多个物理节点,如果信息在传输过程中被劫持,传输的…...
Vue.js组件开发-实现全屏手风琴幻灯片切换特效
使用 Vue 实现全屏手风琴幻灯片切换特效 步骤概述 创建 Vue 项目:使用 Vue CLI 创建一个新的 Vue 项目。设计组件结构:创建一个手风琴幻灯片组件,包含幻灯片项和切换逻辑。实现样式:使用 CSS 实现全屏和手风琴效果。添加交互逻辑…...
数据库、数据仓库、数据湖有什么不同
数据库、数据仓库和数据湖是三种不同的数据存储和管理技术,它们在用途、设计目标、数据处理方式以及适用场景上存在显著差异。以下将从多个角度详细说明它们之间的区别: 1. 数据结构与存储方式 数据库: 数据库主要用于存储结构化的数据&…...
MLM之MiniCPM-o:MiniCPM-o的简介(涉及MiniCPM-o 2.6和MiniCPM-V 2.6)、安装和使用方法、案例应用之详细攻略
MLM之MiniCPM-o:MiniCPM-o的简介(涉及MiniCPM-o 2.6和MiniCPM-V 2.6)、安装和使用方法、案例应用之详细攻略 目录 MiniCPM-o的简介 0、更新日志 1、MiniCPM-o系列模型特点 MiniCPM-o 2.6 的主要特点 MiniCPM-V 2.6的主要特点 2、MiniCPM-o系列模型架构 MiniC…...
【Conda 和 虚拟环境详细指南】
Conda 和 虚拟环境的详细指南 什么是 Conda? Conda 是一个开源的包管理和环境管理系统,支持多种编程语言(如Python、R等),最初由Continuum Analytics开发。 主要功能: 包管理:安装、更新、删…...
Rust 控制流语法详解
Rust 控制流语法详解 控制流是编程语言中用于控制代码执行顺序的重要机制。Rust 提供了多种控制流语法,包括条件判断(if、else if)、循环(loop、while、for)等。本文将详细介绍这些语法,并通过示例展示它们…...
VLC-Qt: Qt + libVLC 的开源库
参考链接 https://blog.csdn.net/u012532263/article/details/102737874...
洛谷 P5146 最大差值 C语言
P5146 最大差值 - 洛谷 | 计算机科学教育新生态 题目描述 HKE 最近热衷于研究序列,有一次他发现了一个有趣的问题: 对于一个序列 A1,A2,…,An,找出两个数 i,j(1≤i<j≤n),使得 Aj−Ai 最大。…...
Zabbix 推送告警 消息模板 美化(钉钉Webhook机器人、邮件)
目前网络上已经有很多关于Zabbix如何推送告警信息到钉钉机器人、到邮件等文章。 但是在搜索下来,发现缺少了对告警信息的美化的文章。 本文不赘述如何对Zabbix对接钉钉、对接邮件,仅介绍我采用的美化消息模板的内容。 活用AI工具可以减轻很多学习、脑力负…...
MySQL数据库环境搭建
下载MySQL 官网:https://downloads.mysql.com/archives/installer/ 下载社区版就行了。 安装流程 看b站大佬的视频吧:https://www.bilibili.com/video/BV12q4y1477i/?spm_id_from333.337.search-card.all.click&vd_source37dfd298d2133f3e1f3e3c…...
书生大模型实战营7
文章目录 L1——基础岛提示词工程实践什么是Prompt(提示词)什么是提示工程提示设计框架CRISPECO-STAR LangGPT结构化提示词LangGPT结构编写技巧构建全局思维链保持上下文语义一致性有机结合其他 Prompt 技巧 常用的提示词模块 浦语提示词工程实践(LangGPT版)自动化生成LangGPT提…...
Spark的基本概念
个人博客地址:Spark的基本概念 | 一张假钞的真实世界 编程接口 RDD:弹性分布式数据集(Resilient Distributed Dataset )。Spark2.0之前的编程接口。Spark2.0之后以不再推荐使用,而是被Dataset替代。Datasetÿ…...
langchain基础(二)
一、输出解析器(Output Parser) 作用:(1)让模型按照指定的格式输出; (2)解析模型输出,提取所需的信息 1、逗号分隔列表 CommaSeparatedListOutputParser:…...
读取要素类中的几何信息
在arcpy中,每个要素都有相关的集合对象,都可以在游标中访问.本节将使用SearchCursor和Polyon对象来读取面要素类几何信息. 操作方法 1.打开IDLE,新建一个脚本 2.导入arcpy模块 3.设置输入要素类为目标面要素类 infc "<>" 4.传入输入要素类创建SearchCurs…...
洛谷 P1130 红牌 C语言
题目描述 某地临时居民想获得长期居住权就必须申请拿到红牌。获得红牌的过程是相当复杂,一共包括 N 个步骤。每一步骤都由政府的某个工作人员负责检查你所提交的材料是否符合条件。为了加快进程,每一步政府都派了 M 个工作人员来检查材料。不幸的是&…...
五. Redis 配置内容(详细配置说明)
五. Redis 配置内容(详细配置说明) 文章目录 五. Redis 配置内容(详细配置说明)1. Units 单位配置2. INCLUDES (包含)配置3. NETWORK (网络)配置3.1 bind(配置访问内容)3.2 protected-mode (保护模式)3.3 port(端口)配置3.4 timeout(客户端超时时间)配置3.5 tcp-keepalive()配置…...
LeetCode题练习与总结:有效三角形的个数--611
一、题目描述 给定一个包含非负整数的数组 nums ,返回其中可以组成三角形三条边的三元组个数。 示例 1: 输入: nums [2,2,3,4] 输出: 3 解释:有效的组合是: 2,3,4 (使用第一个 2) 2,3,4 (使用第二个 2) 2,2,3示例 2: 输入: nums [4,2,3,4] 输出: 4 提示: 1 &…...
【multi-agent-system】ubuntu24.04 安装uv python包管理器及安装依赖
uv包管理器是跨平台的 参考sudo apt-get update sudo apt-get install -y build-essential我的开发环境是ubuntu24.04 (base) root@k8s-master-pfsrv:/home/zhangbin/perfwork/01_ai/08_multi-agent-system# uv venv 找不到命令 “uv”,但可以通过以下软件...
【自然语言处理(NLP)】深度学习架构:Transformer 原理及代码实现
文章目录 介绍Transformer核心组件架构图编码器(Encoder)解码器(Decoder) 优点应用代码实现导包基于位置的前馈网络残差连接后进行层规范化编码器 Block编码器解码器 Block解码器训练预测 个人主页:道友老李 欢迎加入社…...
STM32单片机学习记录(2.2)
一、STM32 13.1 - PWR简介 1. PWR(Power Control)电源控制 (1)PWR负责管理STM32内部的电源供电部分,可以实现可编程电压监测器和低功耗模式的功能; (2)可编程电压监测器(…...
毕业设计:基于卷积神经网络的鲜花花卉种类检测算法研究
目录 前言 课题背景和意义 实现技术思路 一、算法理论基础 1.1 卷积神经网络 1.2目标检测算法 二、 数据集 2.1 数据集 2.2 数据扩充 三、实验及结果分析 3.1 实验环境搭建 3.2 模型训练 最后 前言 📅大四是整个大学期间最忙碌的时光,一边要忙着备考或…...
DeepSeek-R1模型1.5b、7b、8b、14b、32b、70b和671b有啥区别?
deepseek-r1的1.5b、7b、8b、14b、32b、70b和671b有啥区别?码笔记mabiji.com分享:1.5B、7B、8B、14B、32B、70B是蒸馏后的小模型,671B是基础大模型,它们的区别主要体现在参数规模、模型容量、性能表现、准确性、训练成本、推理成本…...
云原生(五十三) | SQL查询操作
文章目录 SQL查询操作 一、数据库DDL操作 1、登陆数据库 2、创建DB数据库 二、数据表DDL操作 1、创建数据表 2、RDS中SQL查询操作 三、SQL查询操作 1、RDS中SQL查询操作 SQL查询操作 一、数据库DDL操作 1、登陆数据库 2、创建DB数据库 创建一个普通账号,…...
Ubuntu 下 nginx-1.24.0 源码分析 - ngx_strerror_init()函数
目录 ngx_strerror_init()函数声明 ngx_int_t 类型声明定义 intptr_t 类型 ngx_strerror_init()函数实现 NGX_HAVE_STRERRORDESC_NP ngx_strerror_init()函数声明 在 nginx.c 的开头引入了: #include <ngx_core.h> 在 ngx_core.h 中引入了 #include <ngx_er…...
【HTML入门】Sublime Text 4与 Phpstorm
文章目录 前言一、环境基础1.Sublime Text 42.Phpstorm(1)安装(2)启动Phpstorm(3)“启动”码 二、HTML1.HTML简介(1)什么是HTML(2)HTML版本及历史(3)HTML基本结构 2.HTML简单语法(1)HTML标签语法(2)HTML常用标签(3)表格(4)特殊字符 总结 前言 在当今的软件开发领域,…...
亲和传播聚类算法应用(Affinity Propagation)
亲和传播聚类算法应用(Affinity Propagation) 亲和传播(Affinity Propagation,简称 AP)是一种基于“消息传递”的聚类算法,与 K-Means 等传统聚类方法不同,它不需要用户预先指定簇的数量&#…...
【VM】VirtualBox安装CentOS8虚拟机
阅读本文前,请先根据 VirtualBox软件安装教程 安装VirtualBox虚拟机软件。 1. 下载centos8系统iso镜像 可以去两个地方下载,推荐跟随本文的操作用阿里云的镜像 centos官网:https://www.centos.org/download/阿里云镜像:http://…...
pytorch实现文本摘要
人工智能例子汇总:AI常见的算法和例子-CSDN博客 import numpy as npfrom modelscope.hub.snapshot_download import snapshot_download from transformers import BertTokenizer, BertModel import torch# 下载模型到本地目录 model_dir snapshot_download(tians…...
大数据相关职位介绍之一(数据分析,数据开发,数据产品经理,数据运营)
大数据相关职位介绍之一 随着大数据、人工智能(AI)和机器学习的快速发展,数据分析与管理已经成为各行各业的重要组成部分。从互联网公司到传统行业的数字转型,数据相关职位在中国日益成为推动企业创新和提升竞争力的关键力量。以…...
Vue3.0实战:大数据平台可视化(附完整项目源码)
文章目录 创建vue3.0项目项目初始化项目分辨率响应式设置项目顶部信息条创建页面主体创建全局引入echarts和axios后台接口创建express销售总量图实现完整项目下载项目任何问题都可在评论区,或者直接私信即可。 创建vue3.0项目 创建项目: vue create vueecharts选择第三项:…...