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

Linux:信号的生命周期分析,以及捕捉信号时中断触发的内核态拦截与用户态处理时机

Linux中常见的信号如下:

其中1-31号信号为普通信号,也是我们需要介绍的重点,而后面部分则为实时信号,我们目前暂时不需要了解。

一,信号的快速认识

我们先来看下面的一个代码样例:

int main()
{
    while(true)
    {
        std::cout << "Hello, world!" << std::endl;
        sleep(1);
    }
    return 0;
}

图中所示的是一个简单的死循环,一旦运行便会每隔一秒打印,但是我们可以通过按下ctrl+c向进程传递一个二号信号终止进程:

为什么这么断定ctrl+c是向进程传递的是一个2号信号呢,我们下面来认识一个函数:

1.1signal函数

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t hanlder);

        他需要我们传入一个返回值为空,参数位为整形的一个函数指针,也就是自定义信号捕捉函数。即当进程收到signum信号时,它会去执行我们自定义的处理方法,下面我们写一个样例来验证我们上面是否收到了2号信号:

void handler(int sig)
{
    std::cout << "收到信号:" << sig << std::endl;
}


int main()
{
    signal(SIGINT, handler);
    while(true)
    {
        std::cout << "Hello, world!" << std::endl;
        sleep(1);
    }
    return 0;
}

        我们注意到一个细节,信号处理方式是在死循环外被改变的,但执行死循环的过程中却能直接跳转到2号的信号处理函数执行,这其实是因为主函数与信号函数分属于不同的执行流,我们在介绍线程的时候再来详细介绍他。 

1.2信号概念

信号是进程之间的一种异步通知的方式,属于软中断。

1.2.1信号的查看方式 

想要像文章开头那样查看所有的信号,需要使用以下命令:

kill -l

如果我们想要看每个信号他们自己对应的默认处理(信号处理的方式有三种:默认,忽略和自定义,下面我们会介绍)行为:

man 7 signal

1.3信号处理方式

1.默认处理

(下面我们均使用signal函数) 

signal(SIGINI,SIG_DFL);//SIGINI为2号信号,而输入SIG_DFL则是让2号信号的捕捉方式为默认

具体某个信号如何默认处理可以使用我们使用上面介绍的命令来查看。

2.忽略处理 

signal(SIGINI,SIG_IGN);//忽略信号处理

        这个就是字面意思,除了我们的SIGCHLD(子进程终止后向父进程发送的信号)比较特殊它的默认处理方式是忽略,但忽略的处理方式是父进程收到子进程的SIGCHLD信号后,等待子进程退出,自动避免子进程成为僵尸进程,其他的信号基本上就是直接忽略当前信号。

        需要注意的是,为了防止恶意进程忽略所有信号来达到自己不被杀掉的目的,有两个信号是无法被忽略处理的,即9号和19号。读者可自行验证。

3.自定义处理

        不多赘述,就是我们上面演示的那种方式,后面我们会介绍其他能够改变信号处理方式的接口,但他们都是通过改变hander表来改变信号的捕捉处理方式的。

二.产生信号的几种方式 

2.1通过终端按键来产生信号 

2.1.1基本操作 

1.Ctrl+C已经验证过,这里不再演示。

2.Ctrl + \(SIGQUIT)可以发送终止信号并生成coredump文件,用于事后的调试。但细心的读者会发现,如果我们使用的是云服务器,会没有coredump文件的生成,这是因为它默认禁止了它的生成,我们后面详细介绍。

3.Ctrl+C可以发送停止信号,将前台进程挂起到后台。(其实有些类似于我们手机上的后台应用)。

2.1.2简要了解OS如何从键盘获取数据 

        当我们按下键盘时,会产生一个硬件中断到中断控制器,数据存储在中断控制器中的寄存器中。然后由中断控制器通知内存,内存再访问中断控制器,获取中断号之后访问中断向量表。找到键盘处理的方法。后面我们会详细介绍硬件中断的全过程。

2.2调用系统命令向进程发送指定信号

        我们可以使用kill + 指定信号名 + 进程pid向对应的进程发送指定信号。继续以上面写的死循环为例,我们让他进行死循环时同时打印自己的pid号:

void handler(int sig)
{
    std::cout << "收到信号:" << sig << std::endl;
}


int main()
{
    signal(SIGINT, handler);
    while(true)
    {
        std::cout << "我是一个死循环,我的pid是" << getpid()<< std::endl;
        sleep(5);
    }
    return 0;
}

2.3通过调用函数向进程发送信号

1.kill函数 

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int sig)

 2.rasie函数

#include <signal.h>
int rasie(int sig);//不需要获取pid,直接向其所在的进程发送sig信号

3.abort函数

#include <stdlib.h>
void abort(void);//直接向当前进程发送一个6号信号,默认终止当前进程

 2.4软件条件产生信号

        软件产生的信号我们之前也接触过比如SIGPIPE(管道相关),但是展现过于麻烦。我们这里换另一种方式来观察软件条件产生信号,就是SIGALARM,他通过alarm函数进行调用:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);//参数为闹钟设定的时间长度单位为s
                                         //返回值为上一个设定闹钟剩余的时间

我们在上面的死循环中加上一个闹钟,并对闹钟行为进行自定义,得到如下结果:

        闹钟设定的是10秒,死循环中每隔5秒打印一条消息。当然如果我们需要设置重复闹钟。可以在其自定义函数中进行设置。 

2.5硬件异常产生信号 

        硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执⾏了除以0的指令, CPU的运算单元会产⽣异常, 内核将这个异常解释为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程。

        除0和访问野指针的情况我们不再模拟,我们重点来介绍下core dump。细心的读者能够发现,我们在1.2.1中的图上每个信号还有一个action,我们介绍下其中常见的三种:

  1. term(进程收到该信号,会直接终止当前进程,且不生成核心转储文件)
  2. ign(进程收到信号之后默认忽略该信号) 
  3. core(core dump)(当进程接受到信号时,会终止进程并生成一个核心转储文件,核心转储文件包含了进程终止时内存的状态)

        而核心转储文件则是在我们调试的时候使用的,我们在使用gdb调试时,使用core-file + 生成的core文件名则可以直接跳转到出错的位置并看到错误的信息。 

我们可以使用ulimit -a命令查看core dump的相关情况:

        我使用的是云服务器,默认禁止core文件的生成。原因就是core file size大小设置为了0。如果我们想要开启该功能,使用ulimit -c -1024(改变core文件大小上限,一般我们设置为1024)即可开启。

三.保存信号 

  1. 实际执行信号的处理动作称为信号递达(Delivery)
  2. 信号从产生到递达之间的状态,称为信号未决(Pending)。
  3. 进程可以选择阻塞 (Block )某个信号。
  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  5. 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

3.1信号在内核中的表示

        每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动
作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上
图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

        常规信号在递达之前产生多次只计一次。上图中SIGINT信号被阻塞,在阻塞状态下无论发多少次SIGINT信号,之后某一时段解除阻塞也只会执行一次它对应的handler操作。实时信号在递达之前产生多次可以依次放在一个队列里。我们暂时不讨论实时信号。

3.2sigest_t 

       从上图来看,每个信号只有一个bit的未决标志, 非0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集 , 这个类型可以表示每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞, 而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)这里的“屏蔽”应该理解为阻塞而不是忽略。 

3.3信号集函数 

        sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态, 至于这个类型内部如何存储这些
bit则依赖于系统实现, 从使用者的角度是不必关心的, 使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释, 比如用printf直接打印sigset_t变量是没有意义的。 

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  1. 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  2. 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
  3. 函数sigaddest与sigdelset会将对应位置的信号置为有效或无效状态。
  4. 以上这四个函数都是成功返回0,出错返回-1。
  5. sigismember则是检验对应信号在信号集中是否为有效状态,若包含则返回1,不包含则返回0,出错返回-1。

        注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

3.3.1sigprocmask

调⽤函数 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参数的可选值。

        如果调用sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀个信号递达。

3.3.2sigpending

#include <signal.h>
int sigpending(sigset_t *set);
//读取当前进程的未决信号集(pending表),通过set参数传出。
//调⽤成功则返回0,出错则返回-1

四.捕捉信号

4.1信号捕捉的流程

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

        上图是信号处理的图示全流程。我们举个例子来解释这张图。比如用户程序注册了SIGQUIT 信号的处理函数 sighandler 。当前正在执行main 函数,这时在1号位置发⽣中断或异常切换到内核态。在中断处理完毕后要返回⽤⼾态的main函数之前检查到有信号SIGQUIT递达。内核决定从2号位置返回用户态后不是恢复 main 函数的上下⽂继续执行,而是执行sighandler 函数, sighandler 和 main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个独立的控制流程,sighandler 函数返回后⾃动执⾏特殊的系统调用sigreturn从4号位置再次进⼊内核态。如果没有新的信号要递达,这次再从4号位置返回用户态就是恢复 main 函数的上下⽂继续执行了。

4.2sigaction函数

sigaction函数其实是我们上面介绍的signal函数的plus版本,函数使用方法如下: 

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
//sigaction函数可以读取和修改与指定信号相关联的处理动作。调⽤成功则返回0,出错则返回- 1。
//signo是指定信号的编号。若act指针⾮空,则根据act修改该信号的处理动作。若oact指针⾮空, 则
//通过oact传出该信号原来的处理动作。

其中struct sigaction结构体结构如下:

struct sigaction {
    void (*sa_handler)(int);      /* 信号处理函数或 SIG_IGN、SIG_DFL */
    void (*sa_sigaction)(int, siginfo_t *, void *); /* 详细信号处理函数 */
    sigset_t sa_mask;             /* 处理该信号时需要阻塞的信号集 */
    int sa_flags;                 /* 标志,影响信号的行为 */
};

        其中sa_sigaction与sa_flags是用来处理实时信号的,我们目前使用时将其置为 nullptr 与 0 即可。而sa_handler则是用户自定义函数,和signal一样是改变signo信号的处理方式。而sa_mask,如果我们在调⽤信号处理函数时,除了当前信号被⾃动屏蔽之外,还希望⾃动屏蔽另外⼀些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时⾃动恢复原来的信号屏蔽字

4.3了解操作系统的运行原理

4.3.1硬件中断 

        先说操作系统需要硬件中断的原因:通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询。

        再来说硬件中断。我们以键盘输入数据为例来解释上图,当我们按下按键时,键盘内部电路检测到某个按键被按下,生成一个扫描码(Scan Code)。这个扫描码寄存在键盘的寄存器中(硬件也有寄存器的概念),然后,它向PIC(可编程中断控制器)/IOAPIC 发送 IRQ 1(键盘中断)。此时在CPU会检测到 IRQ 1,并根据 IDT(中断描述符表) 进行处理。CPU暂停当前正在执行的进程,并跳转到内核的中断处理程序。最后内核解析扫描码交给系统,然后系统在终端显示按键,cpu再继续中断之前的进程。

4.3.2时钟中断

        进程可以在操作系统的指挥下,被调度,被执⾏,那么操作系统⾃⼰被谁指挥,被谁推动执⾏呢?外部设备可以触发硬件中断,但是这个是需要⽤⼾或者设备⾃⼰触发,有没有⾃⼰可以定期触发的设备?

// 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();
} v
oid schedule(void)
{
...
switch_to(next); // 切换到任务号为next 的任务,并运⾏之。
}

        这样,操作系统通过时钟中断,不就在硬件的推动下,⾃动调度了么。所以我们说操作系统是基于中断运行的!!! 

4.3.3操作系统的本质是一个死循环

我们来看一下linux的源码部分: 

void main(void) /* 这⾥确实是void,并没错。 */
{ /* 在startup 程序(head.s)中就是这样假设的。 */
...
/*
* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返
* 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任
* 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时),
* 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没
* 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。
*/
for (;;)
pause();
} // end main

        所以操作系统此时就可以“躺平”了,⾃⼰不做任何事情,需要什么功能,就向中断向量表⾥⾯添加⽅法即可。操作系统的本质:就是⼀个死循环!

        而我们电脑上的主频,其实就是时钟中断的间隔时间。而我们之前说的进程的时间片,时间片的实际长度由操作系统的调度策略决定,可能会根据进程的优先级、类型(I/O 密集型或计算密集型)以及系统负载等因素进行动态调整。而 CPU 主频是硬件特性,决定了 CPU 执行指令的速度。因此,时间片长度和 CPU 主频共同影响进程的执行效率和系统的响应速度。这也正是主频越快,cpu越快的原因。

4.3.4软件原因触发中断 

        为了让操作系统⽀持进⾏系统调⽤,CPU也设计了对应的汇编指令(int 0x80(32位系统) 或者 syscall(64位系统)),可以让CPU内部触发中断逻辑。
        系统调⽤的过程,其实就是先int 0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的方法系统调用号的本质:数组下标:

// sys.h
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
extern int sys_setup (); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137)
extern int sys_fork (); // 创建进程。 (kernel/system_call.s, 208)
extern int sys_read (); // 读⽂件。 (fs/read_write.c, 55)
extern int sys_write (); // 写⽂件。 (fs/read_write.c, 83)
extern int sys_open (); // 打开⽂件。 (fs/open.c, 138)
extern int sys_close (); // 关闭⽂件。 (fs/open.c, 192)
extern int sys_waitpid (); // 等待进程终⽌。 (kernel/exit.c, 142)
extern int sys_creat (); // 创建⽂件。 (fs/open.c, 187)
extern int sys_link (); // 创建⼀个⽂件的硬连接。 (fs/namei.c, 721)
extern int sys_unlink (); // 删除⼀个⽂件名(或删除⽂件)。 (fs/namei.c, 663)
extern int sys_execve (); // 执⾏程序。 (kernel/system_call.s, 200)
extern int sys_chdir (); // 更改当前⽬录。 (fs/open.c, 75)
extern int sys_time (); // 取当前时间。 (kernel/sys.c, 102)
extern int sys_mknod (); // 建⽴块/字符特殊⽂件。 (fs/namei.c, 412)
extern int sys_chmod (); // 修改⽂件属性。 (fs/open.c, 105)
extern int sys_chown (); // 修改⽂件宿主和所属组。 (fs/open.c, 121)
extern int sys_break (); // (-kernel/sys.c, 21)
extern int sys_stat (); // 使⽤路径名取⽂件的状态信息。 (fs/stat.c, 36)
extern int sys_lseek (); // 重新定位读/写⽂件偏移。 (fs/read_write.c, 25)
extern int sys_getpid (); // 取进程id。 (kernel/sched.c, 348)
extern int sys_mount (); // 安装⽂件系统。 (fs/super.c, 200)
extern int sys_umount (); // 卸载⽂件系统。 (fs/super.c, 167)
extern int sys_setuid (); // 设置进程⽤⼾id。 (kernel/sys.c, 143)
extern int sys_getuid (); // 取进程⽤⼾id。 (kernel/sched.c, 358)
extern int sys_stime (); // 设置系统时间⽇期。 (-kernel/sys.c, 148)
extern int sys_ptrace (); // 程序调试。 (-kernel/sys.c, 26)
extern int sys_alarm (); // 设置报警。 (kernel/sched.c, 338)
extern int sys_fstat (); // 使⽤⽂件句柄取⽂件的状态信息。(fs/stat.c, 47)
extern int sys_pause (); // 暂停进程运⾏。 (kernel/sched.c, 144)
extern int sys_utime (); // 改变⽂件的访问和修改时间。 (fs/open.c, 24)
extern int sys_stty (); // 修改终端⾏设置。 (-kernel/sys.c, 31)
extern int sys_gtty (); // 取终端⾏设置信息。 (-kernel/sys.c, 36)
extern int sys_access (); // 检查⽤⼾对⼀个⽂件的访问权限。(fs/open.c, 47)
extern int sys_nice (); // 设置进程执⾏优先权。 (kernel/sched.c, 378)
extern int sys_ftime (); // 取⽇期和时间。 (-kernel/sys.c,16)
extern int sys_sync (); // 同步⾼速缓冲与设备中数据。 (fs/buffer.c, 44)
extern int sys_kill (); // 终⽌⼀个进程。 (kernel/exit.c, 60)
extern int sys_rename (); // 更改⽂件名。 (-kernel/sys.c, 41)
extern int sys_mkdir (); // 创建⽬录。 (fs/namei.c, 463)
extern int sys_rmdir (); // 删除⽬录。 (fs/namei.c, 587)
extern int sys_dup (); // 复制⽂件句柄。 (fs/fcntl.c, 42)
extern int sys_pipe (); // 创建管道。 (fs/pipe.c, 71)
extern int sys_times (); // 取运⾏时间。 (kernel/sys.c, 156)
extern int sys_prof (); // 程序执⾏时间区域。 (-kernel/sys.c, 46)
extern int sys_brk (); // 修改数据段⻓度。 (kernel/sys.c, 168)
extern int sys_setgid (); // 设置进程组id。 (kernel/sys.c, 72)
extern int sys_getgid (); // 取进程组id。 (kernel/sched.c, 368)
extern int sys_signal (); // 信号处理。 (kernel/signal.c, 48)
extern int sys_geteuid (); // 取进程有效⽤⼾id。 (kenrl/sched.c, 363)
extern int sys_getegid (); // 取进程有效组id。 (kenrl/sched.c, 373)
extern int sys_acct (); // 进程记帐。 (-kernel/sys.c, 77)
extern int sys_phys (); // (-kernel/sys.c, 82)
extern int sys_lock (); // (-kernel/sys.c, 87)
extern int sys_ioctl (); // 设备控制。 (fs/ioctl.c, 30)
extern int sys_fcntl (); // ⽂件句柄操作。 (fs/fcntl.c, 47)
extern int sys_mpx (); // (-kernel/sys.c, 92)
extern int sys_setpgid (); // 设置进程组id。 (kernel/sys.c, 181)
extern int sys_ulimit (); // (-kernel/sys.c, 97)
extern int sys_uname (); // 显⽰系统信息。 (kernel/sys.c, 216)
extern int sys_umask (); // 取默认⽂件创建属性码。 (kernel/sys.c, 230)
extern int sys_chroot (); // 改变根系统。 (fs/open.c, 90)
extern int sys_ustat (); // 取⽂件系统信息。 (fs/open.c, 19)
extern int sys_dup2 (); // 复制⽂件句柄。 (fs/fcntl.c, 36)
extern int sys_getppid (); // 取⽗进程id。 (kernel/sched.c, 353)
extern int sys_getpgrp (); // 取进程组id,等于getpgid(0)。(kernel/sys.c, 201)
extern int sys_setsid (); // 在新会话中运⾏程序。 (kernel/sys.c, 206)
extern int sys_sigaction (); // 改变信号处理过程。 (kernel/signal.c, 63)
extern int sys_sgetmask (); // 取信号屏蔽码。 (kernel/signal.c, 15)
extern int sys_ssetmask (); // 设置信号屏蔽码。 (kernel/signal.c, 20)
extern int sys_setreuid (); // 设置真实与/或有效⽤⼾id。 (kernel/sys.c,118)
extern int sys_setregid (); // 设置真实与/或有效组id。 (kernel/sys.c, 51)

// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
}

// 调度程序的初始化⼦程序。
void sched_init(void)
{
...
// 设置系统调⽤中断⻔。
set_system_gate(0x80, &system_call);
} _
system_call:
cmp eax,nr_system_calls-1 ;// 调⽤号如果超出范围的话就在eax 中置-1 并退出。
ja bad_sys_call
push ds ;// 保存原段寄存器值。
push es
push fs
push edx ;// ebx,ecx,edx 中放着系统调⽤相应的C 语⾔函数的调⽤参数。
push ecx ;// push %ebx,%ecx,%edx as parameters
push ebx ;// to the system call
mov edx,10h ;// set up ds,es to kernel space
mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。
mov es,dx
mov edx,17h ;// fs points to local data space
mov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下⾯这句操作数的含义是:调⽤地址 = _sys_call_table + %eax * 4。参⻅列表后的说明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了⼀个包括72 个
;// 系统调⽤C 处理函数的地址数组表。
call [_sys_call_table+eax*4]
push eax ;// 把系统调⽤号⼊栈。
mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。
;// 下⾯97-100 ⾏查看当前任务的运⾏状态。如果不在就绪状态(state 不等于0)就去执⾏调度程
序。
;// 如果该任务在就绪状态但counter[??]值等于0,则也去执⾏调度程序。
cmp dword ptr [state+eax],0 ;// state
jne reschedule
cmp dword ptr [counter+eax],0 ;// counter
je reschedule
;// 以下这段代码执⾏从系统调⽤C 函数返回后,对信号量进⾏识别处理。
ret_from_sys_call:

        而我们平时见不到int 0x80与syscall则是因为Linux的gnu C标准库,给我们把⼏乎所有的系统调⽤全部封装了。方便我们直接调用。

        额外插一嘴,int 0x80与syscall我们能显示调用吗?可以,调用后陷入内核,传入你想要使用的系统调用号,只要合法,便能像我们使用系统调用那样去执行对应系统调用。当然不合法系统会做安全检查,直接拒绝你的调用访问。

4.3.5软中断分类

        我们对于这种调用int 0x80与syscall的产生软中断的方式又称为陷阱,因为是用户自己主动触发的中断。而缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。

4-4如何理解用户态与内核态

 

        用户态就是执行用户[0,3]GB时所处的状态,内核态就是执行内核[3,4]GB时所处的状态,操作系统⽆论怎么切换进程,都能找到同⼀个操作系统,因为所有进程的3-4GB的地址空间是共享的!换句话说操作系统系统调⽤⽅法的执⾏,是在进程的地址空间中执⾏的!

        关于特权级别,涉及到段,段描述符,段选择⼦,DPL,CPL,RPL等概念, ⽽现在芯⽚为了保证兼容性,已经⾮常复杂了,进⽽导致OS也必须得照顾它的复杂性,这块我们不做深究了。而页表部分,下篇文章我们介绍线程时是重点。

        最后,有没有发现我们中断的处理过程与什么很像?bigno-信号。信号其实就是基于操作系统的中断衍生出来的产物。

五,可重入函数 

        这里我们只简单的引出概念,到介绍线程时我们会再介绍。当进程中的不同执行流调用相同的函数时,此时被调用的该函数就是一个可重入函数。在本文的情况就是主函数和信号的自定义函数同时调用同一个函数。而函数是可重入函数必须同时不具备以下条件:

  1. 调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。
  2. 调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。

 


 

 


http://www.kler.cn/a/589774.html

相关文章:

  • 下拉菜单+DoTween插件
  • Houdini :《哪吒2》神话与科技碰撞的创新之旅
  • C语言经典代码题
  • 从 YOLOv1 到 YOLOv2:目标检测的进化之路
  • 轨迹规划:基于查找的(search-based)路径规划算法
  • C#特性和反射
  • MySQL高频八股——事务过程中Undo log、Redo log、Binlog的写入顺序(涉及两阶段提交)
  • 异常(11)
  • Linux 日志与时间同步指南
  • 2024浙江大学计算机考研上机真题
  • 【蓝桥杯】省赛:神奇闹钟
  • 自然语言处理(2)—— NLP之百年风雨路
  • Android第三次面试(Java基础)
  • 蓝牙系统的核心组成解析
  • Secs/Gem第一讲 · 总结精华版(基于secs4net项目的ChatGpt介绍)
  • TypeScript类型兼容性 vs JavaScript动态类型:深入对比解析
  • redis分片集群如何解决高并发写问题的?
  • 【2025年3月最新】Cities_Skylines:城市天际线1全DLC解锁下载与教程
  • 对项目进行优化
  • STL——vector