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

西安哪个公司网站建设好北京互联网公司排行榜

西安哪个公司网站建设好,北京互联网公司排行榜,怎么做公司网站优化,下载好模板该怎么做网站文章目录 前言#xff1a;引入信号生活中的例子信号概念见一见Linux中的信号 浅度理解信号信号处理#xff08;浅谈#xff09;:如何自定义捕捉 信号保存#xff08;浅谈#xff09; 信号产生系统调用产生异常产生#xff1a;浅谈除0异常浅谈解引用野指针异常Core 引入信号生活中的例子信号概念见一见Linux中的信号 浅度理解信号信号处理浅谈:如何自定义捕捉 信号保存浅谈 信号产生系统调用产生异常产生浅谈除0异常浅谈解引用野指针异常Core Term历史问题关于core dump 信号保存补充概念在内核中的表示关于sigset_t类型信号集处理函数设置阻塞信号集获取未决信号集 信号处理内核态 用户态 聊聊信号捕捉谈谈键盘输入数据的过程谈谈如理解操作系统的正常运行如何理解系统调用为什么操作系统能一直运行 可重入函数volatileSIGCHLD信号 前言 上一部分我们主在研究进程间通信的问题我们了解到进程间通信在文件级的通行方式是依赖管道进行通信而对于内核级别可以使用System V的通行方式比如共享内存、消息队列和信号量。 而本文讲解的是Linux信号这一块相关知识而我们首先要知道的是System V下的信号量和Linux信号是两个独立的概念它们之间没有任何关系。 引入信号 生活中的例子 信号在生活中随时可以产生。并且信号的产生和我是异步的我们能识别信号我们知道当信号产生了我们该如何处理我们可以将这个信号进行忽略暂不处理并且我得记住这个信号然后在合适的时候去处理 现在将“我”换成“进程””生活中“换成”操作系统中“。 因此本质上我们还是在研究信号科技源于生活生活中我们会接触到来自各个地方的信号那么在计算机操作系统中同样也存在着多多少少的信号这些信号标志着某个动作 信号概念 Linux系统提供的一种向指定进程发送特定事件的方式。进程会对信号做识别处理并且时刻记住信号产生是异步的 见一见Linux中的信号 输入指令 kill -l 即可查看当前的全部信号 在这之中呢 1~31号信号是我们的普通信号也是我们本章的重点 而34~64好信号是我们的实时信号 对于这么多信号每个信号又代表着一个动作那我们也应该查看每个信号对应的具体功能吧 我们可以输入指令 man 7 signal然后往下拉就可以查看每个信号的具体功能 对于指定的动作就是最左边的描述而中间的core和term我们后续再谈这两者的区别。 现在拿(2)号新号SIGINT来举例这个信号对应的动作是Interrupt from keyboard翻译过来就是从进程接收到来自键盘的终止信号这个信号本质就是我们经常在键盘上输入的组合键[Ctrl C] 浅度理解信号 根据我们上面所引入的信号在生活中的例子我们在执行信号的方式按顺序归结可以有 先是有信号产生接下来是信号保存最后再是信号处理。 为什么我们要有信号保存呢——其实对于信号处理上来说当我正在做一件事情时我会先暂时忽略信号等我处于合适的时候我才会去接受你因此我们当然需要对信号进行保存。 对于这三种方式我想先对它们浅谈让大家有一个大致的轮廓然后我们再深入攻破每种方式。 信号处理浅谈: 当进程接受到信号时必然是要进行处理的而对信号进行处理又分了三种方式进行处理 默认处理忽略处理自定义处理一般都是在捕捉信号 对于这三种方式进程只会从中选一种进行处理。一般来说进程处理信号都是默认处理的而默认处理通常是“终止自己”、“暂停”什么的这也就是为什么我们在将所以信号显示出来是用的kill指令也许当时命名就是根据这个来考虑的。 如何自定义捕捉 信号可以被自定义捕捉进程接收到信号后可以不执行本来应该执行的任务而是去执行自己定义的任务。 首先我们要先认识一个系统调用函数signal #include signal.h typedef void (*sighandler_t)(int); // 函数指针sighandler_t signal(int signum, sighandler_t handler);对于第一个参数signum就是信号的编号比如(2)号信号 第二个参数handler则是一个函数指针这个函数形式与我们在学习qsort函数的参数一致也是存在一个函数指针在参数里。作用更改进程持有的函数指针数组中信号对应下标位置的函数指针所指向的函数为指定函数。捕捉到指定信号之后去执行该函数指针所指向的函数并且要将捕捉到的信号作为参数传递给该函数。 并且这个函数只要在代码里就是一直存在的所以你可能执行过了这行代码但是只要你收到了(2)号信号就会立马被捕捉 下面我们就来用一用这个系统调用函数 #include iostream #include signal.h #include unistd.hvoid handler(int sig) {std::cout std::endl;std::cout 哈哈[ctrl c] 对我无效 std::endl;std::cout 你发的( sig )号信号无法终止我 std::endl;std::cout std::endl; }int main() {// 捕捉(2)号信号signal(2, handler);while(true){sleep(1);std::cout 你好我是进程- getpid() std::endl;std::cout 请按[ctrl c]终止我 std::endl;}return 0; }我们不难看出当我们按下[Ctrl c]后产生的(2)号信号就被headler函数捕捉走了这使得我们一旦产生了(2)号信号就会直接被迫执行headler函数的内容既然我们无法结束这个进程那就只能用我们在学习“进程的状态”那篇博客中处理孤儿进程所运用到的指令kill -9 ‘进程pid’来kill掉指定的进程。 信号保存浅谈 既然信号是针对进程的因此每个进程肯定是可以与信号进行交涉和保存的既然要保存了那么进程内部的PCB不就也要将信号管理起来吗那么谈及管理永远都是**“先描述、再组织”。 因此实际上每个PCB中都会有一个位图**而这个位图有31位每一位记录了一个信号。 简单来说呢就是当我这个进程接收到了(2)号信号就代表着我会将我PCB里的位图的第2个bit位由0修改成1。这样我们就是利用位图来保存了我们的信号。 通过这个我们也可以得知其实根本没有所谓的发送信号本质就是在进程内部的PCB**“写信号”**罢了而唯一能对PCB进行修改的也只能是一个人—— 操作系统 信号产生 通过刚刚的代码示例其实我们不难发现我们平常在写使用Linux时就一直在发送信号。 为什么我会这么说呢回顾以下我们是不是经常的使用[Ctrl c]呢而[Ctrl c]其实是我们通过键盘实现的所以我们可以通过键盘实现。补充一下其实我们按[Ctrl /]也可以终止进程发送的其实是(3)号信号。 上面说的是通过硬件的方式产生字体但其实我们也可以通过系统调用的方式产生信号 系统调用产生 使用kill函数kill 命令其实是通过调用系统调用 kill 函数实现的。 #include signal.h #include sys/types.hint kill(pid_t pid, int sig);功能向 pid 指定的进程发送 sig 所指定的信号。参数pid 表示的就是进程的 pid表示要对哪个进程发送信号。sig 表示要对 pid 所指向的进程发送的信号编号。返回如果信号发送成功则返回 0失败则返回 -1并设置错误码。 测试实例 // testsig.cc#include iostream #include signal.h #include unistd.hvoid handler(int sig) {std::cout std::endl;std::cout 哈哈[ctrl c] 对我无效 std::endl;std::cout 你发的( sig )号信号无法终止我 std::endl;std::cout std::endl; }int main() {// 捕捉(2)号信号signal(2, handler);while(true){sleep(1);std::cout 你好我是进程- getpid() std::endl;std::cout 请按[ctrl c]终止我 std::endl;}return 0; }// myKill.cc#include iostream #include string #include sys/types.h #include signal.h// ./mykill -9 pid int main(int argc, char *argv[]) {if (argc ! 3){std::cout please enter more command std::endl;return 1;}int sig std::stoi(argv[1] 1);pid_t pid std::stoi(argv[2]);kill(pid, sig);return 0; } 使用raise给进程本身发送任意信号 #include signal.hint raise(int sig);参数sig 任意信号编号。返回成功返回 0失败返回 -1 并设置错误码。 测试实例 #include iostream #include signal.h #include unistd.hvoid headler(int sig) {std::cout std::endl;std::cout Hey! I am function headler, I have catched (2)signal std::endl;std::cout std::endl; }int main() {std::cout I am a process and my pid is getpid() std::endl;signal(2, headler);std::cout I will keep send (2)signal to me std::endl;while(true){sleep(1);raise(2);}return 0; }使用abort是当前进程异常终止 #include stdlib.hvoid abort(void);功能向调用该函数的进程发送 SIGABRT (6 号) 信号引起异常终止。 使用alarm设定“闹钟” #include unistd.hunsigned int alarm(unsigned int seconds);功能在特定的时间发送(14)号信号SIGALRM返回值代表上一个闹钟还有几秒响 测试实例 #include iostream #include signal.h #include unistd.hint main() {alarm(5);sleep(1);int n alarm(3); // alarm 的返回值代表上一个闹钟还有几秒响std::cout n std::endl;sleep(1);int cnt 0;while (true){std::cout cnt std::endl;cnt;}return 0; }在这里如果你想尝试把所以信号捕抓当然是可以的但是对于(9)号信号你就根本无法捕捉了。 异常产生 我们在讲解进程的时候曾聊过进程的异常而对于异常处理的直观显示出来我们都会认为程序崩溃了那么程序为什么会崩溃呢 ————进程存在非法访问导致OS给进程发送信号。 比如你对一个数进行了除0操作那么OS就会给这个进程发送(8)号信号SIGFPE又比如你对一个野指针进行了解引用的操作那么OS就会给这个进程发送(11)号信号SIGSEGV 这是因为进程接收到了这些异常信号所以就自然的终止了进程。 但其实你可以选择不抓进程而不进行退出但我还是建议你有异常就直接退出。 浅谈除0异常 上图是我们的CPU在CPU内部有许许多多的寄存器其中有一个名为eflag的寄存器。当其他寄存器在执行运算时eflag就会去看看各个寄存器的运算有没有出现除0操作如果在某个寄存器当中我发现了你有除0的动作那我eflag的溢出标记位的值就会由0置为1就代表你这里进行了除0操作。 而操作系统是管理软硬资源的管理者当OS看到CPU的eflag溢出标记位为1就会给当前进程发送(8)号信号。了解了硬件的原理那为什么我这里建议退出呢如果你不推出可能会导致以下问题 高 CPU 使用率由于异常频繁触发进程会陷入不断处理中断的循环导致 CPU 资源被占用从而产生异常高的 CPU 使用率。这种情况可能会影响系统性能特别是当多个进程或线程出现类似问题时系统资源会被大量占用。影响其他进程的性能如果该进程在异常循环中消耗大量 CPU 时间会影响到其他进程的运行时间导致系统整体的性能下降。这在多任务系统中尤其显著因为 CPU 时间被该进程独占其他任务的响应时间可能延迟。系统或进程的崩溃风险虽然单个异常循环一般不会直接导致系统崩溃但在内核中引发类似循环的代码可能会导致系统崩溃或锁死。此外如果异常处理涉及内存分配或资源管理也可能导致内存耗尽或资源枯竭最终导致系统崩溃。 浅谈解引用野指针异常 回顾以下之前经常用到的地址空间这一概念这里我们引入几个新的寄存器和新概念。 CR3寄存器用来查找指向页表的地址。 CR2寄存器也故障线性地址寄存器。 MMU硬件的内存管理单元(Memory manage unit)实现从虚拟地址向物理地址的转化 所以我们对一遍的代码数据比如int a 4在硬件底层就是上图所示的结构。 而对于野指针呢 ————首先野指针是存在自己对应的虚拟地址的它在mm_struct的区域并不明确大部分时候是处于用户空间中而一般来说说在“未初始化数据”区而对于这些数据来说MMU和CR3并不会为它们创建与物理内存的映射简单来说是把它们封锁起来的因为它们太不安全了。 而一旦你尝试去访问比如解引用操作这部分的资源那么经过MMU手上之后发现这明明是个不安全的资源你要让我访问它因此MMU就会把这个野指针的虚拟地址放在CR2寄存器上并且不会再对这个进程做任何处理了。 和上述一样操作系统是软硬件资源的管理者当我操作系统发现了你CPU的CR2寄存器上有数据那我就直接给你发送(11)号信号。同理还是退出好不然又出现会出现高占用CPU的问题这里我就不过多的赘述了。 Core Term 还记得我们一开始打开的man手册关于信号分类的那一栏吗 这里的Action一栏有Term和Core当时我说过我们后面会谈现在是时候谈谈了。 Term就只代表着异常终止 Core不仅会异常终止还会帮我们生成一个Debug文件而这个Debug文件在云服务器上默认是关闭的 我们可以输入指令ulimit -a 查看 如果你想解除关闭可以执行指令ulimit -c 1024 将这个Debug文件生成的权限给打开这样我们在出现像解引用野指针而发送的(11)号信号就会出现Core文件了 在云服务器上默认是关闭的你可以试试多运行几次你会发现多了很多core的Debug文件文件的内容主要存储了一些错误信息这个过程也叫做“核心转储(core dump)”。而假如你未来在公司使用云服务器写了一个脚本并且不会退出的脚本那么假设你开了core dump的权限那么一个可能在你没发现的时候出现了大量的core文件最后磁盘崩溃被迫系统重装。因此为了安全考虑云服务器是会关闭核心转储的权限的 历史问题关于core dump 还记得我们在学习进程等待的时候讲解过查看进程的退出码和退出信号那一张位图吗其中就有一个1比特位的core dump位我们没有讲解 现在我们可以理解为出现的异常进程如果会发生核心转储(core dump)时core dump位就会置为1我们可以通过代码检测 #include iostream #include signal.h #include unistd.h #include sys/types.h #include sys/wait.hint main() {pid_t pid fork();if(pid 0){// childint *p nullptr;*p 3;}//fatherint status 0;pid_t rid waitpid(pid, status, 0);if(rid 0){std::cerr error tp wait std::endl;return 1;}std::cout exit code: ((status 8) 0xFF) std::endl;std::cout exit signal: (status 0x7F) std::endl;std::cout core dump position: ((status 7) 0x01) std::endl;return 0; }信号保存 补充概念 前面我们有对如何进行信号保存有过浅度理解其实就是在我们每个PCB中都会有一张位图而这个位图记录了每一个信号的产生与否这一部分我们就来好好聊聊这个位图。 补充三个概念 实际上进程接收到信号后对信号做的处理动作默认处理、忽略处理、自定义捕捉称为信号递达信号从产生到递达之间的状态称为信号未决(pending)进程可以选择**阻塞(block)**某个信号。 一旦某个信号被阻塞了那么对应的信号一旦产生了永不递达一直未决直到主动接触阻塞 一个信号如果阻塞和它是否未决无关 在内核中的表示 在每个进程的PCB内部都会维护这三张表实际上之前我们浅谈的在位图中写信号展开来说就是上图所示的 block表 是一张位图和pending表一样 比特位的位置代表信号的编号 比特位的内容代表信号是否阻塞pending表 也是一张位图和block一样 比特位的位置代表信号的编号 比特位的内容代表信号是否可以“收到”handler表 类型为sighandler_t handler[]是一个函数指针数组。 数组的下标就是信号的编号 数组的内容函数指针指向处理的动作。 我们们从左往右看对于(1)号信号block表为0就代表未被封锁pending表为1就代表可以收到对于的处理动作为默认处理。 同理而对于(2)号信号SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞 所以如何让进程识别信号呢实际上我们通过 两张位图 一张函数指针数组 就能做到 被阻塞的信号产生时将保持未决状态知道进接触对此资源的阻塞才执行递达的动作。 注意阻塞和忽略是不同的只要信号被阻塞就不会递达而忽略是在抵达之后可选的一种处理操作。 关于sigset_t类型 sigset_t是一个结构体结构体内部存在一个数组由于pending表和block表都是位图因此它们可以共用一种数据类型sigset_t来进行存储sigset_t也被称为信号集这个类型可以表示每个表对应位置的有效还是无效sigset_t内部依赖于系统的实现用户并不关心内部因此**用户只能通过OS内部提供的函数接口进行调用** 这些OS提供的接口只能用来在你自己定义的sigset_t类型的位图上进行操作但如果你需要修改/获取当前进程的pending/block表你需要使用特定的接口来实现。 #include signal.hint sigemptyset(sigset_t *set); // 将信号集的全部位变成 0 int sigfillset(sigset_t *set); // 将信号集的全部位变成 1 int sigaddset(sigset_t *set, int signo); // 将指定信号添加到信号集中 (将特定比特位变为 1) int sigdelset(sigset_t *set, int signo); // 将指定信号从信号集中删除 (将特定比特位变为 0) int sigismember(const sigset_t *set, int signo); // 判断信号集的有效信号中是否包含指定信号 (判断特定比特位是否为 1)其中我想讲讲sigismember(const sigset_t *set, int signo) sigismember 函数判断信号集的有效信号中是否包含指定信号 (判断特定比特位是否为 1)若包含则返回 1,不包含则返回 0出错返回-1。 假如我想自己定义一个sigset_t的位图然后将(2)号信号添加到我的位图里 #include iostream #include signal.h #include unistd.h #include sys/types.h #include sys/wait.hint main() {sigset_t myset;// 将信号机的全部位置变成0sigemptyset(myset); // 将(2)信号添加到信号集中sigaddset(myset, 2);// 打印信号集for (int i 1; i 31; i){if(sigismember(myset, i)){std::cout 1;}else {std::cout 0;}}std::cout \n;return 0; }信号集处理函数 在前面讲解了我们上的提供的接口只能用来处理自己定义的位图而这一部分讲解的函数接口均是我们通过自己的位图来处理进程内部的block和pending表的 设置阻塞信号集 #include signal.hint sigprocmask(int how, const sigset_t *set, sigset_t *oldset); // 成功返回 0失败返回 -1how表示准备如何设置当前进程的 block 表。set如果该参数非空则该参数是用来修改当前进程的 block 表的。oldset如果该参数非空则该参数是用来获取当前进程的 block 表的。备份 how的可选参数 选项说明SIG_BLOCKset 当中包含了希望添加到当前进程的 block 表的信号SIG_UNBLOCKset 当中包含了希望从当前进程的 block 表中解除阻塞的信号SIG_SETMASK将当前进程的 block 表设置成 set 的内容 测试实例 #include iostream #include signal.h #include unistd.h #include sys/types.h #include sys/wait.hvoid printset(sigset_t myset) {for (int i 1; i 31; i){if (sigismember(myset, i)){std::cout 1;}else{std::cout 0;}}std::cout \n; }int main() {std::cout Hi I am process: getpid() std::endl;sigset_t myset, oldset;// 将信号集的全部位置变成0sigemptyset(myset);sigemptyset(oldset);// 打印自己的信号集std::cout Initializing myset: ;printset(myset);std::cout adding (2)signal to myset... std::endl;// 将(2)信号添加到信号集中sigaddset(myset, 2);// 打印自己的信号集std::cout Now myset: ;printset(myset);// 封锁(2)号信号sigprocmask(SIG_BLOCK, myset, oldset);std::cout For now, I have blocked (2)signal std::endl;std::cout You can try to enter [Ctrl c] std::endl;while(true){sleep(1);}return 0; }获取未决信号集 #include signal.hint sigpending(sigset_t *set);通过 sigset_t 信号集获取当前进程的 pending 表。 测试实例 #include iostream #include signal.h #include unistd.h #include sys/types.h #include sys/wait.hvoid printset(sigset_t myset) {for (int i 1; i 31; i){if (sigismember(myset, i)){std::cout 1;}else{std::cout 0;}}std::cout \n; }int main() {std::cout Hi I am process: getpid() std::endl;sigset_t myset, oldset;// 将信号集的全部位置变成0sigemptyset(myset);sigemptyset(oldset);// 打印自己的信号集std::cout Initializing myset: ;printset(myset);std::cout adding (2)signal to myset... std::endl;// 将(2)信号添加到信号集中sigaddset(myset, 2);// 打印自己的信号集std::cout Now myset: ;printset(myset);// 封锁(2)号信号sigprocmask(SIG_BLOCK, myset, oldset);std::cout For now, I have blocked (2)signal std::endl;std::cout You can try to enter [Ctrl c] std::endl;sleep(3);// 获取当前未决信号集sigset_t newset;sigemptyset(newset);sigpending(newset);std::cout \n;if (sigismember(newset, 2)){std::cout haha, you cant terminate me std::endl;std::cout \n;}else{std::cout dude, why not press [Ctrl c]??? std::endl;std::cout \n;}std::cout now the pending set: ;printset(newset);sleep(2);int cnt 5;std::cout std::endl;std::cout Just wait 5s then you can press[Ctrl c] std::endl;while (true){sleep(1);std::cout Only need: cnt s std::endl;if (cnt 0){std::cout success to unblock (2)signal!!! std::endl;// 利用之前备份的oldset解除对(2)号信号的封锁sigprocmask(SIG_SETMASK, oldset, myset);while(true){// 等待用户按下[Ctrl c]}}cnt--;}return 0; }情况一最后按[Ctrl c] 情况二先按[Ctrl c] 以上我分演示了两种关于我按下[Ctrl c]的不同时机第一种情况还好理解但是对于第二种我们发现当一解除对(2)号信号block我就直接退出了 原理其实很简单我的pending表都已经写上了(2)号信号了你一解除了那我不得立马执行吗 所以当某个信号被pending了一旦解除屏蔽一般就会立即处理该信号 而现在我很好奇是不是我一解除屏蔽我的pending表对于的信号位置就被设置为0了呢 所以我设置了个自定义捕捉的函数signal并写了一个自定义的headler。 然后我分别在headler内部和刚被解除封锁的位置获取pending表。 经过测试发现一接触完就先被signal抓走了并且还会把pending表对应的信号位设置为0 pending位图在递达之前就会被清为0 信号处理 其实我们一直有在做信号处理信号递达的定义不就是进程接收到信号后对信号做的处理动作默认处理、忽略处理、自定义捕捉吗。 而信号可能不会立马被处理而是会在合适的时候被处理。 具体的时候就是在“当进程从内核态返回到用户态的时候进行处理”。 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了 。 如果进程对信号的处理是默认或者忽略那么在内核态经过检验后就会直接返回当前代码的下一行这没什么好说的。 但当你如果是什么利用signal进行信号捕捉的话那操作系统就会从内核态回到用户态然后执行你的headler至于为什么要回到用户态呢首先还是那句话————“操作系统不信任任何人用户”万一我的用户在headler里写满了类似删除系统重要文件的代码怎么办所以得进入用户态执行。 而至于为什么还得先回到内核态再返回用户态的下一条语句呢 恢复执行上下文 当信号发生时操作系统会保存当前进程的执行上下文包括寄存器状态、程序计数器等以便稍后能够恢复。为了进入用户定义的信号处理程序内核会将这一上下文替换为信号处理程序的上下文。在处理完成后sigreturn 的主要任务就是从内核态恢复原始的执行上下文以确保程序能准确恢复到信号到达前的状态。 内核态权限控制和资源管理 信号处理会涉及到一些特权操作如修改用户态的栈指针和寄存器状态这些操作只有在内核态下才允许执行。sigreturn 会使用内核态的权限来恢复原始上下文包括对程序计数器PC的恢复确保信号处理结束后正确返回到用户态的下一条指令执行。 保证系统的一致性和安全性 如果直接从用户态跳转到下一条指令内核的堆栈、寄存器等关键状态可能无法得到有效恢复。这不仅会影响当前进程的状态还可能导致系统资源泄露或异常状态。而通过 sigreturn 在内核态进行状态恢复系统可以确保所有资源和状态的一致性。 异常退出处理 在内核中调用 sigreturn 还能让内核检查信号处理是否正常完成。这样当信号处理程序出现问题例如被中断或出错时系统可以采取相应的补救措施如结束该进程避免潜在的错误继续影响系统稳定性。 内核态 用户态 用户态User Mode 定义用户态是普通应用程序运行的模式。所有应用程序如浏览器、文本编辑器等都在用户态下运行。权限用户态的权限受到严格限制无法直接访问硬件资源如内存、CPU、设备等只能通过操作系统提供的接口如系统调用间接操作硬件。安全性由于权限受限用户态中的程序即便出错或被恶意修改也不会影响到操作系统的核心和其他进程的安全性。 内核态Kernel Mode 定义内核态是操作系统内核代码运行的模式。在这种模式下操作系统拥有对系统全部资源的控制权。权限内核态拥有最高权限可以直接访问硬件、管理内存和控制所有资源。职责内核态负责执行所有特权操作如进程管理、内存管理、设备控制和文件系统操作等。 而对于各自的地址空间来说 用户态在 32 位系统中用户态通常只能使用 0~3GB 的虚拟地址空间而内核态使用剩余的 1GB在 64 位系统中由于虚拟地址空间的扩展用户态和内核态的地址空间布局更灵活。每个用户进程拥有独立的虚拟地址空间相互隔离以确保进程之间不会干扰彼此的数据。 内核态拥有整个地址空间的访问权并可以在部分架构中以高地址段例如 3GB~4GB 或更高映射进入内核虚拟地址空间。内核态地址空间对所有进程来说是共享的因为内核代码和数据需要在所有进程中保持一致。 简单来说内核级页表在整个操作系统中只有一张因此无论进程如何调度CPU 都能直接找到 OS。 聊聊信号捕捉 sigaction函数sigaction 是一个更强大和灵活的信号处理函数可以设置更多的信号处理选项。它通过 struct sigaction 结构体来定义处理程序并提供了更详细的信号处理控制。 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);signum 是信号编号。act 是指向包含信号处理程序的 sigaction 结构体的指针。oldact 是指向旧信号处理程序的指针可用于恢复之前的信号处理行为。 简单测试一下 #include stdio.h #include signal.hvoid handler(int sig) {printf(Caught signal %d\n, sig); }int main() {struct sigaction action;action.sa_handler handler;sigemptyset(action.sa_mask);action.sa_flags 0;sigaction(SIGINT, action, NULL); // 使用 sigaction 设置自定义处理程序while (1); // 无限循环return 0; }最后的结果我们也是疯狂的按[ctrl c]也没有任何作用所以其实它和signal在这方面是一致的 但是为什么要聊聊他呢以及为什么要有他呢 ————其实在很久以前unix上对于我们之前学习的signal函数它存在一个缺陷signal 的行为可能不一致。例如它可能会在信号处理程序执行时自动重置为默认处理程序。这意味着每次收到信号后都需要重新设置处理程序。换言之我就只能捕捉一次而后面你再整个[ctrl c]我就会直接推出了而对于函数sigaction却不会出现该缺陷。 而在现代 Linux 系统中signal 函数的行为已改进不再会自动重置信号处理程序。对于 SIGINT 这样的信号处理函数会在多次触发时继续保持有效因此你可以连续按 CtrlC 而不会触发默认终止操作。 但是有几点我想说一下 如果当前正在对n号进程进行处理默认n号信号会被自动屏蔽。对n号信号处理完成的时候会自动接触对n号信号的屏蔽。 我们也很好的可以验证一般来说当我自定义捕捉了(2)号信号那我一旦输入了[Ctrl c]我就会被自动捕捉然后我的pending表对应的(2)号比特位会由1置为0代表已经实现递达。我可以先在自定义捕捉函数内部设置sleep(30),然后当我再次按下[Ctrl c]我的pending表对应的(2)号比特位就会变为1然后我的block表对应的(2)号比特位也会被置为1。 代码实例 #include iostream #include signal.h #include unistd.h #include sys/types.h #include sys/wait.hvoid printset(sigset_t myset) {for (int i 1; i 31; i){if (sigismember(myset, i)){std::cout 1;}else{std::cout 0;}}std::cout \n; }void handler(int sig) {std::cout you cant determinate me!!! std::endl;while(true){sigset_t pending_set, block_set;sigemptyset(pending_set);sigemptyset(block_set);sigpending(pending_set);std::cout peding set: ;printset(pending_set);sleep(1);} }int main() {std::cout Hi I am process: getpid() std::endl;struct sigaction act, oact;act.sa_handler handler;sigemptyset(act.sa_mask);act.sa_flags 0;sigaction(2, act, oact);std::cout I have cathched (2)signal std::endl;std::cout you can try to press [Ctrl c] std::endl;while(true){}return 0; }我退出是靠[Ctrl \]进行退出的本质是给进程发送了(3)号信号你可以在代码里增加 sigaddset(act.sa_mask, 3)实现对(3)号进程的捕捉这样你发送(3)号信号也没有用。 谈谈键盘输入数据的过程 首先我们需要思考一下操作系统是如何检测键盘的呢如果操作系统无时无刻的在检测键盘是否输入了那假设我只开机并不对键盘操作而操作系统却一直在检测那岂不是会浪费很多时间和资源吗因此我们不可能这样去想硬件和操作系统因为它们是不会采用这么笨的方法的 事实上键盘是通过中断的方法来实现的 中断是一种机制使计算机系统可以在正常执行过程中暂停当前任务去响应紧急或重要的事件。中断发生时处理器会暂时中断当前的指令执行转而去处理中断事件。处理完中断后处理器会恢复之前的任务继续执行。这种机制用于快速响应外部或内部的各种事件是现代计算机系统实现多任务处理的关键。 而8259芯片是一个可编程中断控制器PIC, Programmable Interrupt Controller用于管理和控制多个中断请求信号以便在处理器上实现中断优先级和中断管理。 而每个硬件都会有自己的中断号键盘也会有自己对应的中断号而对键盘实现操作实现的方法是在操作系统中的既然是在操作系统当中那我OS就会去找你的中断号然后通过中断号寻找对应专属于你的方法来实现。 在操作系统当中有一个中断向量表它的本质是一个函数指针里面不仅包含了各个硬件执行操作对应的方法以及还有系统调用的方法。 键盘产生中断请求 键盘在按键按下或松开时通过中断控制器如8259 PIC向CPU发送一个中断请求通常对应的是中断请求线路 IRQ1。 中断控制器处理中断请求 中断控制器8259接收到键盘的中断请求后判断该中断的优先级并将对应的中断号通常是 IRQ1 映射为中断号 0x21发送给CPU。CPU通过INTA信号确认中断并从中断控制器获取中断号。 CPU识别并跳转中断向量表 CPU获取中断号如0x21后会查找中断向量表IVT找到与该中断号对应的中断服务程序ISRInterrupt Service Routine的地址。CPU跳转到该中断服务程序的地址开始执行从而进入操作系统内核。 操作系统处理键盘中断 操作系统的键盘中断服务程序会从键盘控制器读取按键数据并将这些数据放入缓冲区中以供后续使用。该中断处理完成后操作系统向中断控制器发送EOIEnd of Interrupt信号表示中断处理结束。 恢复原有程序执行 CPU返回到被中断的程序继续执行除非有更高优先级的任务或中断需要处理。 当键盘触发中断时通过中断控制器将中断号传递给CPUCPU通过中断号在中断向量表中找到相应的中断服务程序由操作系统负责执行具体的中断处理逻辑。这就是键盘输入数据在底层做的基础流程。 谈谈如理解操作系统的正常运行 如何理解系统调用 本质上和硬件发送中断号然后操作系统拿着中断号去中断向量表中找方法一样没区别。都是拿着一个号码去表里拿方法来实现。 区别在于我使用系统调用是从用户的角度来使用的也就是要以用户态的模式去内核态找到系统调用函数指针表拿取对应方法啊 而原理是这样的当我执行系统调用时进程识别到这是一个系统调用并且将这个系统调用对应的系统调用号记录下来会先调用int 0x80指令从用户态切换成内核态通过指令int 0x80切换内核态的方法也可以称为是一种陷阱。切换为内核态后再将系统调用号存放至CPU的eax寄存器中当操作系统识别到eax有数据时就会通过eax里的系统调用号去寻找对应的方法然后返回至用户态。 在从用户态切换成内核态前一定是要对用户态当前处理的位置有保存处理而通过调用指令int 0x80切换为内核态时int 0x80会导致CPU检查当前代码段的特权级由cs寄存器指示。由于用户程序运行在低特权级Ring 3而int 0x80会将其切换到高特权级Ring 0。cs寄存器保存当前代码段的选择子Segment Selector该选择子包含了当前代码段的特权级别。用户态通常处于较低的特权级Ring 3而内核态运行在最高的特权级Ring 0。而再返回时会从0-3。 CPU会更新cs寄存器将其指向内核的代码段选择子以确保后续执行的是内核代码。一旦cs寄存器被更新CPU会转到内核模式并根据系统调用号执行相应的内核服务例程。内核会根据寄存器中传递的参数执行所请求的操作例如文件读写、进程管理等。 为什么操作系统能一直运行 如果我们的操作系统处理完所有进程后岂不是没有任务可以处理了那操作系统不就会退出了吗 为了保证操作系统永不退出我们会在主板上防止一个时钟装置根据上面了解键盘输入数据的过程时钟会向CPU发送时钟中断然后通过时钟中断号去中断向量表里寻找对应的方法一般是调度方法。 调度方法是干什么的呢一般是一种检测任务负责让操作系统先检查对应进程的时间片如果时间片到了就先暂停对该进程的处理将该进程出队列再入队列。如果时间片没到则忽略。 因此操作系统能一直运行其实是通过时钟中断不断调度任务实现的一个死循环 可重入函数 main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱? 如果一个函数符合以下条件之一则是不可重入的: 调用了malloc或free,因为malloc也是用全局链表来管理堆的。调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。 volatile #include stdio.h #include signal.hint g_flag 0;void handler(int sig) {printf(chage g_flag 0 to 1\n);g_flag 1; } int main() {signal(2, handler);while(!g_flag);printf(process quit normal\n);return 0; }标准情况下键入 CTRL-C ,2号信号被捕捉执行自定义动作修改 flag1 while 条件不满足,退出循 环进程退出 而我一旦在编译的时候输入了g -o1 test.cc 编译然后运行 然后我按下[Ctrl c]被步骤后并不会直接退出。这就是由于我对g选项进行了优化处理。 上图是我一开始的状态CPU寄存器的g_val将内存中的g_val的值拷贝进来当我信号捕捉后内存的值由0修改为1后会将内存的值拷贝至CPU寄存器中然后CPU再识别从而返回。 但当我使用了编译器优化处理后我只会将内存中的g_val修改成1并不会对CPU的值进行修改。 所以程序会一直处于进行不会终止。 SIGCHLD信号 进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。 请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。 事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程 #include stdio.h #include stdlib.h #include signal.h void handler(int sig) {pid_t id;while( (id waitpid(-1, NULL, WNOHANG)) 0){printf(wait child success: %d\n, id);}printf(child is quit! %d\n, getpid()); } int main() {signal(SIGCHLD, handler);cid fork();if( cid 0){//childprintf(child : %d\n, getpid());sleep(3);exit(1);}while(1){printf(father proc is doing some thing!\n);sleep(1);} return 0; }
http://www.dnsts.com.cn/news/155194.html

相关文章:

  • 如何做商城网站小程序广西壮族自治区是哪个省
  • 青岛网站建设企业建站中企动力科技股份有限公司是国企吗
  • 做微信的网站有哪些功能企业网站目的
  • 网站代理服务器设置淮北建设网站
  • 南宁网站建设王道下拉強泰安人事考试网
  • 环境设计案例网站用ps设计一个个人网站模板
  • 网站建设要学哪些软件有哪些方面怎么做网站内容调研
  • 广东省建设注册中心网站郫县建设局网站
  • 怎么给网站做404界面网站建设和管理中 经验
  • 网站建设要实现的目标wordpress响应式修改
  • 哪个网站可以做行程表网站租用服务器多少钱
  • 推荐十个国外网站企业优化网站
  • 自助建站网站平台兼容ie8的网站模板
  • 丰都网站建设c 做网站用什么框架
  • 旅游网站建设论文题目成都网站建设工作室
  • 上海自助模板建站python编程软件安装教程
  • iis7.0搭建网站免费地方网站
  • 网站模板整站怎么做室内设计公司网站
  • 营山网站建设网站的建设目标是什么
  • 禹城网站制作怎么查看网站开发语言的类型
  • 沙田镇网站建设做网站网站建设专业公司
  • 企业做的网站推广费用如何记账家居网站建设平台
  • asp.net网站恢复广告加盟
  • 网站架构图怎么做创办网站需要怎么做
  • 梅州建网站南通网站开发招聘
  • 搜狗网站seo石家庄网站建设接单
  • 青岛响应式网站开发网站开发php程序员
  • 网站推广方法有哪几种淘宝官网首页电脑版
  • 什么网站可以做外链原版百度
  • 制作类网站设计图片素材网站有哪些