内核代码

Posted Jiamings

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了内核代码相关的知识,希望对你有一定的参考价值。

赵炯;《Linux 内核完全注释 0.11 修正版 V3.0》

本章内容主要是 linux/kernel/ 目录下共包括 10 个 C 语言文件,2 个汇编文件和 1 个 Makefile。


文章目录

1. 总体功能

【Linux

1.1 中断处理程序

asm.s:大部分硬件异常所引起的中断汇编语言处理过程(另外几个硬件中断处理程序在文件 system_call.s 和 mm/page.s 中实现)。
trap.c :asm.s 调用的 C 函数。

在用户程序(进程)将控制权交给中断处理程序之前,CPU 会首先将至少 12 字节(EFLAGS、CS、EIP)的信息压入中断处理程序的堆栈中,即进程的内核态栈中(段间子程序调用),CPU 会将代码段选择符和返回地址的偏移值压入堆栈,如果优先级发生了变化,CPU 还会将源代码的堆栈段值和堆栈指针压入中断程序的堆栈中。党发生中断时,中断处理程序使用的是进程的内核态堆栈。

【Linux
asm.s 代码文件主要涉及对 Intel 保留中断 int0–int16 的处理,其余保留的中断 int17-int31 由Intel公司留作今后扩充使用。对应于中断控制器芯片 IRQ 发出的 int32-int47 的 16 个处理程序将分别在各种硬件初始化程序中处理。Linux 系统调用中断 int 0x80 将在 kernel/system_call.s 中给出。

由于有些异常引起中断时,CPU 内部会产生一个出错代码压入堆栈(异常中断 int 8 和 int10 - int 14),而其它的中断却不带有这个出错代码。

对一个硬件异常所引起的中断的处理过程如图:
【Linux

1.2 系统调用处理相关程序

Linux 中应用程序调用内核的功能是通过中断调用 int 0x80 进行的,寄存器 eax 中放调用号,如果需要带参数,则 ebx、ecx 和 edx 用于存放调用参数,因此该中断调用被称为系统调用。实现系统调用的相关文件包括 system_call.s、fork.c、signal.c、sys.c 和 exit.c 文件。

system_call.s 程序的作用类似于硬件中断处理中 asm.s 程序的作用,另外还对时钟中断和硬盘、软盘中断进行处理。而 fork.c 和 signal.c 中的一个函数则类似于 traps.c 程序的作用,它们为系统中断调用提供 C 处理函数。fork.c 程序提供两个 C 处理函数:find_empty_process() 和 cpoy_process()。signal.c 程序还提供一个处理有关进程信号的函数 do_signal(),在系统调用中断处理过程中被调用。另外还包括 4 个系统调用 sys_xxx() 函数。

sys.c 和 exit.c 程序实现了其它一些 sys_xxx() 系统调用函数。这些 sys_xxx() 函数都是相应系统调用所需调用的处理函数,有些是使用汇编语言实现的,如 sys_execve();而另外一些则用 C 语言实现。

通常以 ​​do_​​​ 开头的中断处理过程中调用的 C 函数,要么是系统调用处理过程中通用的函数,要么是某个系统调用专用的;而以 ​​sys_​​ 开头的系统调用函数则是指定的系统调用的专用处理函数。

1.3 其它通用类程序

这些程序包括 ​​schedule.c​​​、​​mktime.c​​​、​​panic.c​​​、​​printk.c​​​ 和 ​​vsprintf.c​

  • ​schedule.c​​ 程序包括内核调用最频繁的 schedule()、sleep_on()、wakeup() 函数,是内核的核心调度程序,用于对进程的执行进行切换或改变进程的执行状态,另外还有包括有关系统时钟中断和软盘驱动器定时的函数。
  • ​mktime.c​​​ 程序中仅包含一个内核使用的时间函数 mktime(),仅在​​init/main.c​​ 中被调用一次。
  • ​panic.c​​ 中包含一个 panic() 函数,用于在内核运行出现错误时显示出错信息并停机。
  • ​printk.c​​​ 和​​vsprintf.c​​ 是内核显示信息的支持程序,实现了内核专用显示函数 printk() 和字符串格式化输出函数 vsprintf()。

2. Makefile

3. asm.s 程序

3.1 功能描述

asm.s 汇编程序中包括大部分 CPU 探测到的异常故障处理的底层代码,也包括数学协处理器的异常处理。该程序于 kernel/traps.c 程序有着密切的关系。该程序的主要处理方式是在中断处理程序中调用 traps.c 中相应的 C 函数程序,显示出错位置和出错号,然后退出中断。

每行 4B。初始位置为 esp0,将需要调用的 C 函数 do_divide_error() 或其它 C 函数地址入栈后,指针位置是 esp1,使用交换指令,将入栈的地址和 eax 寄存器中的内容交换,将一些必要寄存器入栈(保护寄存器内容),前往 esp2。将 esp0 保存到堆栈 esp3 处(为了给调用的 C 函数提供参数 —— 出错位置、出错号),只需要 add 8 即可跳过这两个参数。
【Linux

在C函数返回后,汇编程序需要把先前压入栈中的函数参数清除掉,即调用者负责清除参数占用的栈空间。

3.2 其它信息

Intel 保留中断向量的定义:

【Linux

4. trap.c 程序

4.1 功能描述

trap.c 程序主要包括一些在处理异常故障底层代码 asm.s 文件中调用的相应 C 函数。用于显示出错位置和出错号等调试信息,其中的 die() 通用函数用于在中断处理中显示详细的出错信息,而代码最后的初始化函数 trap_init() 是在前面 init/main.c 中被调用,用于初始化硬件异常处理中断向量(陷阱门),并设置允许中断请求信号的到来。

5. system_call.s 程序

在 Linux 0.11 中,用户使用中断调用 int 0x80 和放在寄存器 eax 中的功能号来使用内核提供的各种功能服务,这些操作系统提供的功能被称之为系统调用功能。通常用户并不是直接使用系统调用中断,而是通过函数库(例如 libc)中提供的接口函数来调用的。例如创建进程的系统调用 fork 可直接使用函数 fork() 即可。函数库 libc 中的 fork() 函数会实现对中断 int 0x80 的调用结果返回给用户程序。

对于所有系统调用的实现函数,内核把它们按照系统调用功能号顺序排列成一张函数指针(地址)表(​​include/linux/sys.h​​ 中)。然后在中断 int 0x80 的处理过程中根据用户提供的功能号调用对应系统调用函数进行处理。

本程序主要实现系统调用(system_call)中断 int 0x80 的入口处理过程以及信号检测处理,同时给出了两个系统功能的底层接口,分别是 sys_execve 和 sys_fork。还列出了处理过程类似的协处理器出错 int 16、设备不存在 int 7、时钟中断 int 32、硬盘中断 int 46、软盘中断 int 38 的中断处理程序。

对于软中断(system_call、coprocessor_error、device_not_available),其处理过程基本上是首先为调用相应 C 函数处理程序做准备,将一些参数压入堆栈。系统调用最多可以带 3 个参数,分别通过寄存器 ebx、ecx、edx 传入。然后调用 C 函数进行相应功能的处理,处理返回后再去检测当前任务的信号位图,对值最小的一个信号进行处理并复位信号位图中的该信号。系统调用的 C 语言处理函数分布在整个 linux 内核代码中,由 include/linux/sys.h 头文件中的系统函数指针数组来匹配。

对于硬件中断请求信号 IRQ 发来的中断,其处理过程首先是向中断控制芯片 8259A 发送结束硬件中断控制字指令 EOI,然后调用相应的 C 函数处理程序。对于时钟中断也要对当前任务的信号位图进行检测处理。

对于系统调用的中断处理过程,可以把它看作是一个接口程序,实际上每个系统调用功能的处理过程基本上都是通过调用相应的 C 函数进行的。

这个程序在刚进入时会首先检查 eax 中的功能号是否有效,然后保存一些会用到的寄存器到堆栈上,Linux 内核默认把段寄存器 ds,es 用于内核数据段,而 fs 用于用户数据段。接着通过一个地址跳转表(sys_call_table)调用相应系统调用的 C 函数。在 C 函数返回后,程序就把返回值压入堆栈保存起来。

接下来,该程序查看执行本次调用进程的状态,如果由于上面 C 函数的操作或其他情况而使进程的状态从执行态变更成了其它状态,或者由于时间片已经用完,则调用进程调度函数 schedule()。由于在执行 jmp_schedule 之前已经把返回地址 ret_from_sys_call 入栈,因此在执行完 schedule() 后最终会返回到 ret_from_sys_call 处继续执行。

从 ret_from_sys_call 标号处开始的代码执行一些系统调用的后处理工作。主要判断当前进程是否是初始进程 0,如果是就直接退出此次系统调用,中断返回。否则再根据代码段描述符和所使用的堆栈来判断本次系统调用的进程是否是一个普通进程,若不是则说明是内核进程或其它。则也立刻弹出堆栈内容退出系统调用中断。末端的一块代码用来处理调用系统调用进程的信号。若进程结构的信号位图表明该进程有接收到信号,则调用信号处理函数 do_signal()。

最后,该程序恢复保存的寄存器内容,退出此次中断处理过程并返回调用程序,若有信号则程序会首先返回到相应信号处理函数中去执行,然后返回调用 system_call 程序。

【Linux

6. mktime.c 程序

该程序只有一个函数 ​​kernel_mktime()​​,仅供内核使用。计算从 1970 年 1 月 1 日 0 时起到开机当日经过的秒数(日历时间),作为开机时间。该函数与标准 C 库中提供的 mktime() 函数的功能完全一样,都是将 tm 结构表示的时间转换成 UNIX 日历时间。但是由于内核不是普通程序,不能调用开发环境库中的函数。

闰年的基本计算方法:如果 y 能够被 4 整除且不能被 100 除尽,或者能被 400 整除,则 y 是闰年。

7. sched.c 程序

P288

  • 进程调度过程

https://www.bilibili.com/video/BV1ub41157LM P22-25

该程序是内核中有关任务(进程)调度管理的程序,其中包括有关调度的基本函数(sleep_on()、wakeup()、schedule()等)以及一些简单的系统调用函数。系统时钟中断处理过程中调用的定时函数 do_timer() 也被放置在本程序中。
【Linux

schedule() 函数

该函数负责选择系统中下一个将要运行的进程。首先对所有任务进行检测,唤醒任何一个已经得到信号的任务。

具体方法是针对任务数组中的每个任务,检查其报警定时值 alarm。如果任务的 alarm 时间已经过期(alarm<jiffies(是系统从开机开始算起的滴答数 10ms/滴答)),则在它的信号位图中设置 SIGALRM 信号,然后清空 alarm 值。在 sched.h 中定义。如果进程的信号位图中除去被阻塞的信号外还有其他信号,并且任务处于可中断睡眠状态(TASK_INTERRUPTIBLE),则置任务为就绪状态(TASK_RUNNING)。

调度函数的核心处理部分。根据进程的时间片和优先权调度机制,来选择随后要执行的任务。首先循环检查任务数组中的所有任务,根据每个就绪任务剩余执行时间的值 counter,选取该值最大的一个任务,然后利用 switch_to() 函数切换到该任务。如果所有就绪态任务的该值都等于零,表示此刻所有任务的时间片都已经运行完,于是就根据任务的优先权值 priority,重置每个任务的运行时间片值 counter,再重新执行循环检查所有任务的执行时间片值。

可编程定时/计数控制器

8259A是专门为了对8085A和8086/8088进行中断控制而设计的芯片,它是可以用程序控制的中断控制器。单个的8259A能管理8级向量优先级中断。在不增加其他电路的情况下,最多可以级联成64级的向量优级中断系统。

8253(8254) 芯片是一个可编程定时/计数器芯片,用于解决计算机中通常碰到的时间控制问题,再软件的控制下产生精确的时间延迟。该芯片提供了 3 个独立的 16 位计数器通道,每个通道可以工作在不同的工作方式下,并且这些工作方式均可以使用软件来设置。

【Linux
【Linux

计数器工作方式:

  1. 计数结束中断方式;
  2. 硬件可触发单次计数方式;
  3. 频率发生器方式;
  4. 方波发生器方式;
  5. 软件触发选通方式;
  6. 硬件触发选通方式;

8. signal.c 程序

signal.c 程序涉及内核中所有有关信号处理的函数。在 UNIX 系统中,信号是一种软件中断处理机制。它提供了一种处理异步事件的方法,当进程设置的一个报警时钟到期时,系统就会向进程发送一个 SIGALRM 信号,当发生硬件异常时,系统也会向正在执行的进程发送相应的信号,一个进程也可以向另一个进程发送信号。

在内核代码中通常使用一个无符号长整数(32位)中的比特位来表示各种不同的信号。因此最多可表示32个不同的信号,参考 include/signal.h 文件。

对于进程来说,收到一个信号,会有如下动作:

  1. 忽略该信号。大多数信号都可以被进程忽略,但是 SIGKILL、SIGSTOP 无法被忽略。
  2. 捕获该信号。告诉内核,在指定的信号发生时调用我们自定义的信号处理函数。
  3. 执行默认操作。内核为每种信号都提供一种默认操作。
    【Linux
    【Linux

9. exit.c 程序

该程序主要描述了子进程终止和退出的有关处理事宜。主要包含进程释放、会话终止和程序退出处理函数以及杀死进程、终止进程、挂起进程等系统调用函数。还包括进程信号发送函数和通知父进程子进程终止的函数。

10. fork.c 程序

fork 系统调用用于创建子进程。Linux 中所有进程都是进程 0 的子进程,该程序是 sys_fork() 系统调用的辅助处理函数集,给出了 sys_fork() 系统调用中使用的两个 C 语言函数:find_empty_process() 和 copy_proces,还包括进程内存区域验证与内存分配函数 verify_area() 和 copy_mem()。

fork 首先会为新进程申请一页内存用来复制父进程的任务数据结构(PCB)信息,然后会为新进程修改复制的任务数据结构的某些字段值,包括利用系统调用中断发生时逐步压入堆栈的寄存器信息重新设置任务结构中 TSS 结构的各字段值,让新进程的状态保持父进程即将进入中断过程前的状态,然后为新进程确定在线性地址空间中的起始位置。对于 CPU 的分段机制,Linux 0.11 的代码段和数据段在线性地址空间中的位置和长度完全相同。接着系统会为新进程复制父进程的页目录项和页表项。对于 Linux0.11 来说,所有程序共用一个位于物理内存开始位置处的页目录表,而新进程的页表则需另行申请一页内存来存放。

在 fork() 的执行过程中,内核并不会立刻为新进程分配代码和数据内存页,新进程将与父进程共同使用父进程已有的代码和数据内存页,只有当以后执行过程中如果其中有一个进程以写方式访问内存时被访问的内存页面才会在写操作前被复制到新申请的内存页面中。

【Linux

11. sys.c 程序

sys.c 程序含有很多系统调用功能的实现函数。该程序中含有很多有关 pid、pgid、uid、gid、ruid、euid、session 等的操作函数。

一个用户有用户ID(uid)、用户组ID(gid)。这两个 ID 是 passwd 文件中对该用户设置的 ID,通常被称为实际用户ID(ruid)和实际组ID(rgid)。而在每个文件的 i 节点信息中都保存着宿主的用户ID和组ID,它们分别指明了文件拥有者和所属用户组。主要用于访问或执行文件时的权限判别操作。
【Linux
进程的 uid 和 gid 分别就是进程拥有者的用户ID和组ID,也即进程的实际用户ID(ruid)和实际组ID(rgid)。超级用户可以使用函数 set_uid() 和 set_gid() 对它们进行修改。有效用户 ID和有效组ID用于进程访问文件时的许可权判断。

vsprintf.c 程序

主要包括 vsprintf 函数,用于对参数产生格式化的输出。不涉及内核工作原理方面的内容。

printk.c 程序

printk() 是内核中使用的打印函数,功能与 C 标准函数库中的 printf() 相同,由于不能使用专用于用户模式的 fs 段寄存器,printk 函数中需要显示的信息是在内核数据段中,printk 函数中需要临时使用 fs 段寄存器,故需要先保存其中的内容,然后调用 tty_write() 进行信息的打印显示。

panic.c 程序

panic() 函数用于显示内核错误信息并使系统进入死循环。在内核程序很多地方,若内核代码在执行过程中出现严重错误时就会调用该函数。


以上是关于内核代码的主要内容,如果未能解决你的问题,请参考以下文章

检测即将发生的文本选择

Py4JJavaError:在尝试将 rdd 数据帧写入本地目录上的镶木地板文件时调用 o389.parquet 时发生错误

网络设备之侦测连接状态

创建在特定事件发生时调用的自定义 appsync 解析器或 lambda 函数

实例化 Swift 类时调用站点出错

使用 mpi_f08 模块时调用 mpi_gatherv 后数组的 Lbound 发生变化