Linux信号之捕捉信号

1.捕捉信号

信号捕捉的流程

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

过程:

用户程序注册了SIGQUIT信号的处理函数sighandler

当前正在执行main函数,这时发送中断或异常切换到内核态

在中断处理完毕后,要返回用户态的main函数之前检查到有信号SIGQUIT递达

内核决定返回用户态后是否恢复main函数上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是俩个独立的控制流程。

sighandler函数返回后,自动执行特殊的系统调用sigreturn,再次进入内核态

如果没有新的信号要递达,这次返回用户态就是恢复main函数的上下文继续执行了

  • 控制流程的独立性

    • main函数的控制流程main函数的执行是线性的,它按照程序的指令顺序执行,通过函数调用和返回来实现模块化的代码执行。它的执行路径是明确的,由程序的逻辑决定。

    • sighandler函数的控制流程 :信号处理函数的执行是由信号触发的,它与main函数的执行路径无关。信号处理函数的执行时机是不确定的,它可能在main函数的任何位置被触发。信号处理函数执行完成后,内核会恢复main函数的上下文,继续执行main函数。

助记图

2.操作系统的运行

1.硬件中断

CPU要知道哪一个外部设备有信息,是通过一个叫中断控制器的,当有外部设备有信息时,就会先发送一个中断号,然后中断控制器就会停通知CPU,然后CPU就会知道哪一个外设有东西,并从中断控制器获取中断号,然后把就当前CPU的内容拷贝到寄存器上进行保存,以便下次执行时,从终止的地方继续往下执行,拿到中断号后,CPU回到一个叫IDT中断向量表,也是一个函数指针数组,存储着不同的方法,然后下标就是中断号,不同的中断号就对应着不同的中断操作,执行完后就会回到CPU。

注意:

中断向量表就是操作系统的一部分,启动就加载到内存中

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

由外部设备触发,中断系统运行流程,叫硬件中断

2.关于初始化中断和陷阱门的代码

补充:

不止CPU有寄存器,外设也有寄存器。

设备寄存器的作用:

  1. 状态寄存器:存储设备当前的状态信息,如是否忙碌、是否有数据可读、是否有错误发生等。

  2. 控制寄存器:允许CPU发送命令给设备,如开始传输、停止传输、复位设备等。

  3. 数据寄存器:在设备和CPU之间传输数据时,数据寄存器用于暂存数据。

  4. 配置寄存器:用于设置设备的运行参数,如波特率、时钟频率、中断级别等。

让CPU执行中断处理方法就是读取键盘数据。

信号与硬件中断

发送中断 --- 发信号

保存中断号 --- 记录信号

中断号 --- 信号编号

处理中断 --- 处理信号,自定义捕捉

可以看出基本流程是一致的,因为信号就是模拟硬件中断

3.时钟中断

问题:

进程可以在操作系统下,被调度,被执行,那么操作系统被谁指挥这样做,被谁推动执行?

外部设备可以进行硬件中断,但是这个需要用户或者设备自己触发,有没有自己可以定期触发的设备?

答案:

外部设备的时钟源,会定期的发送中断信号给中断控制器,也会定期的执行中断向量表对应的方法,而时钟源对应的中断号,在中断向量表中对应的调度的方法,也就是操作系统会在时钟源的推动下一直执行调度,然后时钟源发送中断号的周期就是我们电脑设备上的2.60GMZ("我的"),发送的越快CPU处理进程的速度就越快,电脑就塞雷。

现代的时钟源集成到了CPU里面了。

关于计时

联网后开始记录时间,就有时间戳,因为时钟源的频率知道,就可以计算出历史的总频率,即便离线了也可以通过总频率进行++来同步时间

时间戳(Timestamp)

时间戳是一个用于记录特定事件发生时间的机制。在计算机系统中,时间戳通常是一个自某个特定起点(如系统启动、Unix纪元等)以来的秒数或毫秒数的计数。时间戳可以通过操作系统的时钟或特定的硬件时钟(如RTC,实时时钟)来生成。

时钟源和频率

计算机系统通常有一个或多个时钟源,这些时钟源可以是硬件时钟(如RTC或高精度的晶振)或软件时钟(基于CPU周期的计数)。时钟源的频率是已知的,这意味着系统可以准确地测量时间的流逝。

在线和离线时间同步

  1. 在线同步:当计算机联网时,它可以通过网络时间协议(NTP)或其他时间同步服务与时间服务器同步。这些服务器提供了一个精确的时间参考,计算机可以根据这个参考来调整自己的系统时钟。

  2. 离线同步:当计算机离线时,它仍然可以通过内部时钟源来维持时间的流逝。由于时钟源的频率是已知的,计算机可以通过计数时钟周期来推算时间。这种方法的准确性取决于时钟源的稳定性和精确度。

通过总频率进行时间同步

您提到的"通过总频率进行++来同步时间"可能是指在离线状态下,计算机通过内部时钟源的频率来维持时间的计数。这种方法的基本思想是:

  • 计算机记录下每次时钟周期的计数(即频率)。

  • 即使在离线状态下,计算机也可以通过累加这些周期来推算时间的流逝。

  • 当计算机重新联网时,它可以再次与时间服务器同步,以校正可能的误差。

关于时钟源的代码

解释

`sched_init`函数

  • 这个函数是调度程序的初始化子程序,负责设置时钟中断和系统调用中断。

  • `set_intr_gate(0x20, &timer_interrupt);`:设置中断向量0x20(时钟中断)的处理函数为`timer_interrupt`。这行代码将时钟中断的处理程序与中断向量关联起来。

  • `outb(inb_p(0x21) & ~0x01, 0x21);`:修改中断控制器(8259A芯片)的屏蔽码,允许时钟中断(IRQ0)。这行代码通过清除屏蔽码中的相应位来允许时钟中断。

  • `set_system_gate(0x80, &system_call);`:设置系统调用中断门。这行代码将系统调用的处理程序与中断向量0x80关联起来。

`timer_interrupt`函数

  • 这个函数是时钟中断的处理程序。

  • `call_do_timer();`:调用`do_timer`函数来执行任务切换、计时等工作。`do_timer`函数在`kernel/sched.c`文件的第305行实现。

`do_timer`函数

  • 这个函数是时钟中断处理的核心函数,负责执行任务切换、计时等工作。

  • `schedule();`:调用`schedule`函数来切换到下一个任务。

`schedule`函数

  • 这个函数负责任务调度。

  • `switch_to(next);`:切换到任务号为`next`的任务,并运行之。这行代码将CPU的控制权切换到下一个任务。

总结

这段代码的主要作用是初始化调度程序,设置时钟中断和系统调用中断的处理程序,并在时钟中断发生时执行任务切换和计时等工作。通过这种方式,Linux内核0.11版本实现了多任务调度和时间管理功能。

具体来说:

  1. `sched_init`函数初始化调度程序,设置时钟中断和系统调用中断的处理程序。

  2. `timer_interrupt`函数作为时钟中断的处理程序,调用`do_timer`函数来执行任务切换和计时等工作。

  3. `do_timer`函数是时钟中断处理的核心函数,负责执行任务切换和计时等工作。

  4. `schedule`函数负责任务调度,切换到下一个任务。

通过这些函数的协同工作,Linux内核0.11版本实现了基本的多任务调度和时间管理功能。

4.死循环

操作系统有了时钟源就可以不用干什么了,需要什么功能就向中断向量表添加方法就可以了,操作系统的本质就是一个死循环。

5.时间片的本质就是计数器

时间片示例代码

cpp 复制代码
#include <stdint.h>

// 假设的CPU上下文结构
typedef struct {
    uint32_t eax;
    uint32_t ebx;
    uint32_t ecx;
    uint32_t edx;
    // 其他寄存器...
} Context;

// 假设的任务结构
typedef struct Task {
    Context context;
    int time_slice;  // 时间片大小
    int remaining_time;  // 剩余时间片
} Task;

Task current_task;  // 当前任务
Task next_task;  // 下一个任务

// 模拟的系统调用,用于切换任务
void switch_to(Task *next) {
    // 保存当前任务的上下文
    // 加载下一个任务的上下文
    // 代码省略...
}

// 计时器中断处理函数
void timer_interrupt_handler() {
    // 减少当前任务的剩余时间片
    current_task.remaining_time--;

    // 如果当前任务的时间片用完,切换到下一个任务
    if (current_task.remaining_time <= 0) {
        current_task.remaining_time = current_task.time_slice;  // 重置时间片
        switch_to(&next_task);  // 切换任务
    }

    // 重新启动计时器
    // 代码省略...
}

// 初始化计时器,设置定时器中断
void init_timer() {
    // 设置定时器,使其定期触发中断
    // 代码省略...
}

// 初始化调度程序
void init_scheduler() {
    // 初始化当前任务和下一个任务
    // 设置时间片大小
    current_task.time_slice = 10;  // 假设时间片大小为10个时间单位
    next_task.time_slice = 10;
    current_task.remaining_time = current_task.time_slice;
    next_task.remaining_time = next_task.time_slice;

    // 初始化计时器
    init_timer();
}

// 主函数
int main() {
    // 初始化调度程序
    init_scheduler();

    // 开始执行任务
    while (1) {
        // 执行当前任务
        // 代码省略...

        // 触发计时器中断(模拟)
        timer_interrupt_handler();
    }

    return 0;
}

6.软中断

上述外部硬件中断,需要硬件设备触发

当然也有通过软件的原因,也触发上面逻辑

为了让操作系统也支持进行系统调用,CPU也设计了对应的汇编指令(C和汇编语言可以混编)

int或者syscall,可以让CPU内部触发中断逻辑。

(下图黄框框的是)

问题:

用户层怎么把系统调用号给操作系统?寄存器(如EAX)

操作系统怎么把返回值给用户?寄存器或者用户传入的缓冲区地址

答案:

用户程序将系统调用所需的参数放入相应的寄存器中

用户程序执行软件中断指令(int 0x80或者syscall),从而触发系统调用,在中断向量表根据中断号找到对应的方法

在系统调用结束后,结果通常放在一个约定的寄存器中(如eax),用户可以在该寄存器中获取结果

获取系统调用号 可以通过寄存器eax

系统调用的过程,其实先int 0x80,syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的方法,而这个方法会根据系统调用号,自动查表,执行对应的方法

系统调用号的本质:就是作为系统调用表的下标

gun C标准库

我们用的系统调用,没有见过int 0x80或者syscall,都是直接调用上层的函数,是因为Linux的gun C标准库,给我们把几乎所有的系统调用全部封装了。

补充:

系统调用号(System Call Number)是一个用于标识特定系统调用的整数值。在操作系统中,系统调用号作为索引,用于在系统调用表中查找对应的系统调用处理函数。系统调用表是一个数据结构(通常是一个数组),其中包含了指向各种系统调用处理函数的指针。

系统调用号的作用

  1. **标识系统调用**:每个系统调用都有一个唯一的系统调用号,用于标识该调用。这使得操作系统能够区分不同的系统调用请求。

  2. **简化系统调用处理**:通过使用系统调用号作为索引,操作系统可以快速查找并执行相应的系统调用处理函数,而无需解析复杂的命令或参数。

  3. **扩展性**:新增系统调用时,只需为其分配一个新的系统调用号,并在系统调用表中添加相应的处理函数即可。这种设计使得操作系统可以方便地扩展新的系统调用。

系统调用号的分配

系统调用号通常由操作系统设计者分配,以确保每个系统调用都有一个唯一的标识符。这些编号通常是连续的整数,但也可以是其他形式的标识符,具体取决于操作系统的实现。

示例

在Linux系统中,常见的系统调用号及其对应的系统调用包括:

  • `sys_read`:读取文件或设备的数据,通常对应系统调用号3。

  • `sys_write`:向文件或设备写入数据,通常对应系统调用号4。

  • `sys_open`:打开文件或设备,通常对应系统调用号5。

  • `sys_close`:关闭文件或设备,通常对应系统调用号6。

  • `sys_fork`:创建一个新的进程,通常对应系统调用号2。

系统调用的执行过程

  1. **用户程序准备参数**:用户程序将系统调用所需的参数放入相应的寄存器中。

  2. **设置系统调用号**:用户程序将系统调用号放入特定的寄存器(如x86架构中的`eax`或ARM架构中的`r7`)。

  3. **触发系统调用**:用户程序执行软中断指令(如`int 0x80`或`syscall`),从而触发系统调用。

  4. **查找系统调用处理函数**:操作系统接收到系统调用请求后,根据系统调用号在系统调用表中查找对应的处理函数。

  5. **执行系统调用处理函数**:操作系统调用相应的处理函数执行系统调用。

  6. **返回结果**:系统调用完成后,结果通常放在一个约定的寄存器中(如x86架构中的`eax`或ARM架构中的`r0`),用户程序可以从该寄存器中获取结果。

通过这种方式,系统调用号作为关键的索引值,使得操作系统能够高效、可靠地处理各种系统调用请求。
这段代码定义了一个系统调用函数指针表 `sys_call_table[]`,它在x86架构的Linux操作系统中用于处理通过软中断指令 `int 0x80` 发起的系统调用。这个表作为跳转表,将系统调用号映射到对应的处理函数上。

系统调用函数指针表的作用:

  1. **映射系统调用号到处理函数**:每个系统调用都有一个唯一的编号(系统调用号),这个编号用作 `sys_call_table` 数组的索引,以找到对应的处理函数。

  2. **简化系统调用的分发**:操作系统通过查找这个表来确定哪个函数会被调用,从而简化了系统调用的分发逻辑。

  3. **扩展性**:新增系统调用时,只需在表中添加新的函数指针,并为它分配一个新的系统调用号。

代码解释:

```c

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

};

```

  • `fn_ptr`:这是一个函数指针类型,用于存储指向函数的指针。

  • `sys_call_table[]`:这是一个函数指针数组,每个元素对应一个系统调用的处理函数。

  • 例如,`sys_read` 是读取文件的系统调用,`sys_write` 是写入文件的系统调用,`sys_open` 是打开文件的系统调用,等等。

系统调用的处理:

当用户程序通过 `int 0x80` 指令发起系统调用时,操作系统会捕获这个中断,读取包含系统调用号的寄存器(通常是 `eax`),然后使用这个编号作为索引来查找 `sys_call_table` 数组中的处理函数。找到后,操作系统会调用该函数来执行实际的系统调用操作。

这种机制允许操作系统以一种结构化和模块化的方式来处理各种系统调用,同时也便于添加新的系统调用或修改现有调用的处理逻辑。

7.缺页中断,内存碎片处理与除零野指针错误

缺页中断,内存碎片处理,除零野指针错误,这些问题,全都会转成CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的用来处理内存碎片,有的是用来给目标进程发送信号,杀掉进程等。

操作系统就是躺在中断处理例程上的代码块

CPU内部的软中断,比如int 0x80或者syscall,叫做陷阱

CPU内部的软中断,比如除零/野指针等,叫做异常

补充:

指令集

指令集是CPU能够理解和执行的一组指令。不同的CPU架构有不同的指令集,这些指令集中包含了执行各种操作的指令,包括触发中断的指令。

  • x86架构:在x86架构中,系统调用可以通过int指令触发,其中int 0x80是用于触发系统调用的特定指令。这个指令会导致CPU从用户态切换到内核态,并开始执行系统调用处理程序。
相关推荐
刘一说6 分钟前
CentOS 系统 Java 开发测试环境搭建手册
java·linux·运维·服务器·centos
wdxylb6 小时前
云原生俱乐部-shell知识点归纳(1)
linux·云原生
飞雪20077 小时前
Alibaba Cloud Linux 3 在 Apple M 芯片 Mac 的 VMware Fusion 上部署的完整密码重置教程(二)
linux·macos·阿里云·vmware·虚拟机·aliyun·alibaba cloud
路溪非溪7 小时前
关于Linux内核中头文件问题相关总结
linux
Lovyk9 小时前
Linux 正则表达式
linux·运维
Fireworkitte10 小时前
Ubuntu、CentOS、AlmaLinux 9.5的 rc.local实现 开机启动
linux·ubuntu·centos
sword devil90011 小时前
ubuntu常见问题汇总
linux·ubuntu
ac.char11 小时前
在CentOS系统中查询已删除但仍占用磁盘空间的文件
linux·运维·centos
淮北也生橘1213 小时前
Linux的ALSA音频框架学习笔记
linux·笔记·学习
华强笔记16 小时前
Linux内存管理系统性总结
linux·运维·网络