【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 类型的变量之前,一定要调用 sigemptysetsigfillset 进行初始化,以确保信号集处于一个确定的状态。初始化 sigset_t 变量之后,就可以通过调用 sigaddsetsigdelset 在该信号集中添加或删除某种有效信号。



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 指示如何进行更改。具体来说:
  • 如果 osetset 都是非空指针,则首先将原来的信号屏蔽字备份到 oset 中,然后根据 sethow 参数来更改信号屏蔽字。

假设当前的信号屏蔽字为 maskhow 参数的可选值及其含义如下:


具体来说:

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,&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,&irq13);// 设置协处理器的陷阱⻔。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 的中断⻔向量(硬件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设计了相应的汇编指令(如 intsyscall),使得在没有外部硬件中断的情况下,通过这些指令也能触发中断逻辑。

这样通过软件实现上述逻辑的机制被称为软中断。软中断有固定的中断号,用来索引特定的中断处理程序,常见的形式包括 syscall: XXXint: 0x80

操作系统会在中断向量表中为软中断配置处理方法,并将系统调用的入口函数放置于此。当触发软中断时,会通过这个入口函数找到对应的系统调用函数指针数组,进而匹配并调用具体的系统调用。系统调用表使用系统调用号作为数组下标来查找对应的系统调用。


系统调用过程

系统调用的过程本质上是通过触发软中断(例如 int 0x80syscall),使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语言有关联。



如何理解内核态和用户态

每个进程都有自己的虚拟地址空间,这个地址空间分为几个部分:

  1. 用户区:这部分地址空间是进程私有的,每个进程都有自己独立的一份用户区。用户区包含了进程的代码、数据、堆栈等。
  2. 内核区:这部分地址空间是所有进程共享的,包含了内核代码和数据结构。


用户页表和内核页表
  1. 用户页表

    • 每个进程都有自己独立的用户页表,用于映射用户区的虚拟地址到物理地址。
    • 用户页表确保了每个进程的用户区是独立的,互不影响。
  2. 内核页表

    • 内核页表在整个操作系统中只有一份,所有进程共享这份内核页表,这样所有进程都能看到同一个操作系统(OS)。
    • 内核页表用于映射内核区的虚拟地址到物理地址,确保所有进程都能访问相同的内核数据和代码。


内核页表的作用
  1. 共享内核数据

    • 内核页表使得所有进程都能看到同一个操作系统内核数据和代码,确保了内核功能的一致性和可靠性。
    • 例如,内核数据结构如文件系统、网络协议栈等都是共享的。
  2. 增强进程独立性

    • 尽管内核页表是共享的,但每个进程的虚拟地址空间中都包含了一份内核页表的映射。
    • 这样,进程在进行系统调用或其他内核操作时,可以直接在自己的虚拟地址空间中访问内核数据,而不需要切换到其他地址空间。
    • 这种设计增强了进程的独立性,减少了上下文切换的开销。


简单总结

进程的虚拟地址空间分为两部分:用户区和内核区。用户区包括我们熟知的栈区、堆区、共享区、代码区、数据区等,是每个进程独有的。内核区则是独立的一个区域,用于存放操作系统内核的代码和数据。值得注意的是,内核区资源通常是只读不可修改的,整个操作系统只有一份内核页表,所有进程共享这份内核页表,从而所有进程都能看到同一个操作系统。当进程需要执行程序访问操作系统内核时,可以直接在自己的虚拟地址空间中的内核区访问,这使得操作更为便捷。

以设计者将系统调用都封装成了函数,并集成到了 GNU glibc 库中。



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

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

相关文章

Python 网络爬虫实战:从基础到高级爬取技术

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 1. 引言 网络爬虫&#xff08;Web Scraping&#xff09;是一种自动化技术&#xff0c;利用程序从网页中提取数据&#xff0c;广泛…

Windows程序设计10:文件指针及目录的创建与删除

文章目录 前言一、文件指针是什么&#xff1f;二、设置文件指针的位置&#xff1a;随机读写&#xff0c;SetFilePointer函数1.函数说明2.函数实例 三、 目录的创建CreateDirectory四、目录的删除RemoveDirectory总结 前言 Windows程序设计10&#xff1a;文件指针及目录的创建与…

关于安卓greendao打包时报错问题修复

背景 项目在使用greendao的时候&#xff0c;debug安装没有问题&#xff0c;一到打包签名就报了。 环境 win10 jdk17 gradle8 项目依赖情况 博主的greendao是一个独立的module项目&#xff0c;项目目前只适配了java&#xff0c;不支持Kotlin。然后被外部集成。greendao版本…

设计模式 - 行为模式_Template Method Pattern模板方法模式在数据处理中的应用

文章目录 概述1. 核心思想2. 结构3. 示例代码4. 优点5. 缺点6. 适用场景7. 案例&#xff1a;模板方法模式在数据处理中的应用案例背景UML搭建抽象基类 - 数据处理的 “总指挥”子类定制 - 适配不同供应商供应商 A 的数据处理器供应商 B 的数据处理器 在业务代码中整合运用 8. 总…

FlashAttention v1 论文解读

论文标题&#xff1a;FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness 论文地址&#xff1a;https://arxiv.org/pdf/2205.14135 FlashAttention 是一种重新排序注意力计算的算法&#xff0c;它无需任何近似即可加速注意力计算并减少内存占用。…

stm32硬件实现与w25qxx通信

使用的型号为stm32f103c8t6与w25q64。 STM32CubeMX配置与引脚衔接 根据stm32f103c8t6引脚手册&#xff0c;采用B12-B15四个引脚与W25Q64连接&#xff0c;实现SPI通信。 W25Q64SCK&#xff08;CLK&#xff09;PB13MOSI&#xff08;DI&#xff09;PB15MISO(DO)PB14CS&#xff08…

软件工程概论试题五

一、多选 1.好的软件的基本属性包括()。 A. 效率 B. 可依赖性和信息安全性 C. 可维护性 D.可接受性 正答&#xff1a;ABCD 2.软件工程的三要素是什么()? A. 结构化 B. 工具 C.面向对象 D.数据流! E.方法 F.过程 正答&#xff1a;BEF 3.下面中英文术语对照哪些是正确的、且是属…

FBX SDK的使用:基础知识

Windows环境配置 FBX SDK安装后&#xff0c;目录下有三个文件夹&#xff1a; include 头文件lib 编译的二进制库&#xff0c;根据你项目的配置去包含相应的库samples 官方使用案列 动态链接 libfbxsdk.dll, libfbxsdk.lib是动态库&#xff0c;需要在配置属性->C/C->预…

知识库管理在提升企业决策效率与知识共享中的应用探讨

内容概要 知识库管理是指企业对内部知识、信息进行系统化整理和管理的过程&#xff0c;其重要性在于为企业决策提供了坚实的数据支持与参考依据。知识库管理不仅能够提高信息的获取速度&#xff0c;还能有效减少重复劳动&#xff0c;提升工作效率。在如今快速变化的商业环境中…

基于vue船运物流管理系统设计与实现(源码+数据库+文档)

船运物流管理系统目录 目录 基于springboot船运物流管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、管理员登录 2、货运单管理 3、公告管理 4、公告类型管理 5、新闻管理 6、新闻类型管理 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考…

【自然语言处理(NLP)】深度学习架构:Transformer 原理及代码实现

文章目录 介绍Transformer核心组件架构图编码器&#xff08;Encoder&#xff09;解码器&#xff08;Decoder&#xff09; 优点应用代码实现导包基于位置的前馈网络残差连接后进行层规范化编码器 Block编码器解码器 Block解码器训练预测 个人主页&#xff1a;道友老李 欢迎加入社…

Spring Boot 实例解析:配置文件

SpringBoot 的热部署&#xff1a; Spring 为开发者提供了一个名为 spring-boot-devtools 的模块来使用 SpringBoot 应用支持热部署&#xff0c;提高开发者的效率&#xff0c;无需手动重启 SpringBoot 应用引入依赖&#xff1a; <dependency> <groupId>org.springfr…

Linux网络 HTTPS 协议原理

概念 HTTPS 也是一个应用层协议&#xff0c;不过 是在 HTTP 协议的基础上引入了一个加密层。因为 HTTP的内容是明文传输的&#xff0c;明文数据会经过路由器、wifi 热点、通信服务运营商、代理服务器等多个物理节点&#xff0c;如果信息在传输过程中被劫持&#xff0c;传输的…

java练习(5)

ps:题目来自力扣 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这…

深入 Rollup:从入门到精通(三)Rollup CLI命令行实战

准备阶段&#xff1a;初始化项目 初始化项目&#xff0c;这里使用的是pnpm&#xff0c;也可以使用yarn或者npm # npm npm init -y # yarn yarn init -y # pnpm pnpm init安装rollup # npm npm install rollup -D # yarn yarn add rollup -D # pnpm pnpm install rollup -D在…

MySQL数据库环境搭建

下载MySQL 官网&#xff1a;https://downloads.mysql.com/archives/installer/ 下载社区版就行了。 安装流程 看b站大佬的视频吧&#xff1a;https://www.bilibili.com/video/BV12q4y1477i/?spm_id_from333.337.search-card.all.click&vd_source37dfd298d2133f3e1f3e3c…

松灵机器人 scout ros2 驱动 安装

必须使用 ubuntu22 必须使用 链接的humble版本 #打开can 口 sudo modprobe gs_usbsudo ip link set can0 up type can bitrate 500000sudo ip link set can0 up type can bitrate 500000sudo apt install can-utilscandump can0mkdir -p ~/ros2_ws/srccd ~/ros2_ws/src git cl…

【最长上升子序列Ⅱ——树状数组,二分+DP,纯DP】

题目 代码&#xff08;只给出树状数组的&#xff09; #include <bits/stdc.h> using namespace std; const int N 1e510; int n, m; int a[N], b[N], f[N], tr[N]; //f[i]表示以a[i]为尾的LIS的最大长度 void init() {sort(b1, bn1);m unique(b1, bn1) - b - 1;for(in…

Linux安装zookeeper

1, 下载 Apache ZooKeeperhttps://zookeeper.apache.org/releases.htmlhttps://zookeeper.apache.org/releases.htmlhttps://zookeeper.apache.org/releases.htmlhttps://zookeeper.apache.org/releases.htmlhttps://zookeeper.apache.org/releases.htmlhttps://zookeeper.apa…

day6手机摄影社区,可以去苹果摄影社区学习拍摄技巧

逛自己手机的社区&#xff1a;即&#xff08;手机牌子&#xff09;摄影社区 拍照时防止抖动可以控制自己的呼吸&#xff0c;不要大喘气 拍一张照片后&#xff0c;如何简单的用手机修图&#xff1f; HDR模式就是让高光部分和阴影部分更协调&#xff08;拍风紧时可以打开&…