潍坊网站关键词推广,行业资讯网站有哪些,建设医院网站服务,网站开发公司+重庆理解Linux系统内进程信号的整个流程可分为#xff1a; 信号产生 信号保存 信号处理 上篇文章重点讲解了 信号的产生#xff0c;本文会讲解信号的保存和信号处理相关的概念和操作#xff1a; 两种信号默认处理 1、信号处理之忽略
::signal(2, SIG_IGN); // ignore: 忽略#… 理解Linux系统内进程信号的整个流程可分为 信号产生 信号保存 信号处理 上篇文章重点讲解了 信号的产生本文会讲解信号的保存和信号处理相关的概念和操作 两种信号默认处理 1、信号处理之忽略
::signal(2, SIG_IGN); // ignore: 忽略#include vector
#include unistd.h
#include stdlib.h
#include signal.h
#include sys/wait.hvoid 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();}
}运行结果如下 显然对二号信号ctrlc 没有效果了 2、信号处理之默认
::signal(2, SIG_DFL); // default:默认。#include vector
#include unistd.h
#include stdlib.h
#include signal.h
#include sys/wait.h
#include iostream
#include stringvoid handler(int signo)
{std::cout get a new signal: signo std::endl;exit(1);
}int main()
{// 信号捕捉:// 1. 默认// 2. 忽略// 3. 自定义捕捉//::signal(2SIG 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 参数来更改信号屏蔽字。
假设当前的信号屏蔽字为 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、调用对应的中断方法 例如外设磁盘需要将部分数据写到内存当磁盘准备好了通过一个硬件中断中断控制器通知 CPUCPU得知并获取对应的中断号通过该中断号索引中断向量表的对应中断处理服务
操作系统通过该中断服务将磁盘的就绪的数据读入内存 中断向量表就是操作系统的⼀部分启动就加载到内存中了操作系统主函数中含有一个“硬件中断向量表初始化逻辑如下源码展示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 (i17;i48;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 芯⽚的IRQ3IRQ4 中断信号请求。
} 时钟中断 问题
进程可以在操作系统的指挥下被调度被执行那么操作系统自己被谁指挥被谁推动执⾏呢外部设备可以触发硬件中断但是这个是需要用户或者设备自己触发有没有自己可以定期触发的设备? 如下图会有一个硬件时钟源向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()将意味着我们必须等待收到⼀个信号才会返* 回就绪运⾏态但任务0task0是唯⼀的意外情况参⻅schedule()因为任* 务0 在任何空闲时间⾥都会被激活当没有其它任务在运⾏时* 因此对于任务0pause()仅意味着我们返回来查看是否有其它任务可以运⾏如果没* 有的话我们就回到这⾥⼀直循环执⾏pause()。*/for (;;)pause();
}
// end main因此 我们之前写的通过信号模拟实现操作系统的代码中void Handler(int signum) 这个自定义信号处理函数不就可以类似传入中断号索引查询中断向量表执行对应的中断处理函数吗
这样操作系统只需要死循环等待着硬件发来中断再干活
因此操作系统也可以称为通过中断推动运行的进程
#includeiostream
#includefunctional
#includevector
#includeunistd.h
#include signal.h
using namespace std;// 定义一个函数指针类型用于处理信号
typedef void (*sighandler_t)(int);
// 定义一个函数对象类型用于存储要执行的函数
using func functionvoid();
// 定义一个函数对象向量用于存储多个要执行的函数
vectorfuncfuncV;
// 定义一个计数器变量
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 库中。