【linux V0.11】kernel(水)
文章目录
- 悲!
- 中断处理和系统调用
- asm.s
- traps.c
- 系统调用
- 什么是系统调用?
- 系统调用实现机制
- 1. 中断门设置(IDT)
- 2. 用户态调用系统调用
- 3. 内核处理系统调用
- 主要流程
- 系统调用表:`sys_call_table`
- 系统调用的函数命名规则
- 系统调用的参数传递
- 系统调用返回值
- 系统调用与中断的关系
- 系统调用的局限性
- 总结:系统调用关键点
- 任务调度
- 任务调度概述
- 进程状态与调度关系
- 调度器核心函数:`schedule()`
- 调度流程
- 调度器特点总结
- 进程结构体 `task_struct` 中与调度相关的字段
- 关键字段解释
- 时间片分配机制
- 调度触发时机
- 调度器的局限性
- 进程创建与调度关系(`fork()`)
- 总结: 任务调度关键点
悲!
昨天详细记了kernel里中断,系统调用,任务调度相关的代码解读,但是没保存 (我记得我保存了,说什么都晚了) ,我不想再写一遍了,借助ai(介意的不要看了)大概梳理一下。
以后有时间再回来补上这一篇。
中断处理和系统调用
每个中断由0~255之间的一个数字来标识。
中断int0~int31(0x00~0x1f)的功能是由Intel固定设定或保留用的。asm. s 代码文件主要涉及对 Intel 保留中断 int0~int16 的处理,其余保留的中断 int17~int31 由 Intel 公司留作今后扩充使用。
asm.s 用于实现大部分硬件异常所引起的中断的汇编语言处理过程。 而traps.c程序则实现了asm.s的中断处理过程中调用的C函数。 另外几个硬件中断处理程序在文件system_call. s 和 mm/page. s 中实现。
中断int32~int255 (0x20~0xff)可以由用户自己设定。
在Linux系统中,将int32~int47(0x20~0x2f)对应于8259A中断控制芯片发出的硬件中断请求信号IRQ0~IRQ15,这16 个处理程序将分别在各种硬件(如时钟、键盘、软盘、数学协处理器、硬盘等)初始化程序中处理。
把程序编程发出的系统调用(system_call)中断设置为int128(0x80),处理在kernel/system_call. s 中给出,实现系统调用的相关文件包括system_signal. c、sys. c 和 exit. c 文件。
_set_gate
在指定地址(gate_addr)设置一个门描述符(Gate Descriptor),用于响应中断或异常。
// asm/system.h
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \ //将偏移地址低字与段选择符组合成描述符低4字节(eax)。"movw %0,%%dx\n\t" \ //将类型标志字与偏移高字组合成描述符高4字节(edx)。"movl %%eax,%1\n\t" \ //分别设置门描述符的低4字节和高4字节。"movl %%edx,%2" \: \: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \"o" (*((char *) (gate_addr))), \"o" (*(4+(char *) (gate_addr))), \"d" ((char *) (addr)),"a" (0x00080000))#define set_intr_gate(n,addr) \_set_gate(&idt[n],14,0,addr)#define set_trap_gate(n,addr) \_set_gate(&idt[n],15,0,addr)#define set_system_gate(n,addr) \_set_gate(&idt[n],15,3,addr)
名称 | 类型type | DPL | 用途 |
---|---|---|---|
set_intr_gate(n, addr) | 14(中断门) | 0 | 硬件中断(如定时器、键盘) |
set_trap_gate(n, addr) | 15(陷阱门) | 0 | 异常、调试、系统调用入口(内核态) |
set_system_gate(n, addr) | 15(陷阱门) | 3 | 系统调用(用户态可调用) |
陷阱门不会自动屏蔽中断(IF 标志位保持不变),适合用于系统调用,因为系统调用过程中可能还需要响应中断(如时钟中断),更具体的可以去看中断描述符。
set_trap_gate()
与set_system_gate()
主要区别在于前者设置的特权级为0,后者为3。因此断点int3、溢出overflow和边界出错中断bounds可由任何程序产生。
void trap_init(void)
{int i;set_trap_gate(0,÷_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);for (i=17;i<48;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,¶llel_interrupt);
}
asm.s
这些中断处理函数(divide_error,debug,…)的实现都在asm.s
_divide_error:pushl $_do_divide_error
no_error_code:xchgl %eax,(%esp)pushl %ebxpushl %ecxpushl %edxpushl %edipushl %esipushl %ebppush %dspush %espush %fspushl $0 // "error code"lea 44(%esp),%edxpushl %edxmovl $0x10,%edxmov %dx,%dsmov %dx,%esmov %dx,%fscall *%eaxaddl $8,%esppop %fspop %espop %dspopl %ebppopl %esipopl %edipopl %edxpopl %ecxpopl %ebxpopl %eaxiret_debug:pushl $_do_int3 # _do_debugjmp no_error_code_nmi:pushl $_do_nmijmp no_error_code
...
在开始执行程序之前,堆栈指针esp指在中断返回地址一栏(图中esp0处)。 当把将要调用的C函数do_divide_error()或其他 C 函数地址入栈后,指针位置是esp1处,此时通过交换指令,该函数的地址被放入eax寄存器中,而原来eax的值被保存到堆栈上。 在把一些寄存器入栈后,堆栈指针位置在esp2处。 当正式调用do_divide_error()之前,程序将开始执行时的esp0 堆栈指针值压入堆栈,放到了esp3处,并在中断返回弹出入栈的寄存器之前指针通过加上8又回到esp2处。
正式调用do_divide_error()之前把出错代码以及esp0 入栈的原因是为了把出错代码和esp0 作为调用C函数do_divide_error的参数。 在 traps. c 中该函数的原型为:void do_divide_error(long esp, long error_code)
。
当协处理器执行完一个操作时就会发出IRQ13中断信号,以通知CPU操作完成
_irq13:pushl %eaxxorb %al,%al //80387在执行计算时,CPU会等待其操作的完成outb %al,$0xF0//通过写0xF0端口,本中断消除CPU的BUSY延续信号,并重新激活387的处理器扩展请求引脚PEREQ。//该操作主要是为了确保在继续执行387的任何指令之前,响应本中断。movb $0x20,%aloutb %al,$0x20 //向8259主中断控制芯片发送EOI(中断结束)信号。jmp 1f //这两个跳转指令起延时作用
1: jmp 1f
1: outb %al,$0xA0 //再向8259从中断控制芯片发送EOI(中断结束)信号popl %eaxjmp _coprocessor_error //在kernel/system call.s
以下中断处理在调用时会在中断返回地址之后将出错号压入堆栈,因此返回时也需要将出错号弹出。
_double_fault:pushl $_do_double_fault
error_code:xchgl %eax,4(%esp) # error code <-> %eaxxchgl %ebx,(%esp) # &function <-> %ebxpushl %ecxpushl %edxpushl %edipushl %esipushl %ebppush %dspush %espush %fspushl %eax # error codelea 44(%esp),%eax # offsetpushl %eaxmovl $0x10,%eax # 置内核数据段选择符mov %ax,%dsmov %ax,%esmov %ax,%fscall *%ebx # 调用相应的C函数,其参数已入栈addl $8,%esp # 堆栈指针重新指向栈中放置fs内容的位置pop %fspop %espop %dspopl %ebppopl %esipopl %edipopl %edxpopl %ecxpopl %ebxpopl %eaxiret_invalid_TSS:pushl $_do_invalid_TSSjmp error_code_segment_not_present:pushl $_do_segment_not_presentjmp error_code
...
总之,asm. s 中包括大部分CPU探测到的异常故障处理的底层代码,也包括数学协处理器(FPU)的异常处理。
traps.c
asm.s中断汇编代码为trap.c里的错误处理c代码准备参数,然后去调用对应的错误处理函数do_xxx(long esp, long error_code)
(int3除外)。
void do_int3(long * esp, long error_code,long fs,long es,long ds,long ebp,long esi,long edi,long edx,long ecx,long ebx,long eax)
{int tr;__asm__("str %%ax":"=a" (tr):"0" (0)); //取任务寄存器值→trprintk("eax\t\tebx\t\tecx\t\tedx\n\r%8x\t%8x\t%8x\t%8x\n\r",eax,ebx,ecx,edx);printk("esi\t\tedi\t\tebp\t\tesp\n\r%8x\t%8x\t%8x\t%8x\n\r",esi,edi,ebp,(long) esp);printk("\n\rds\tes\tfs\ttr\n\r%4x\t%4x\t%4x\t%4x\n\r",ds,es,fs,tr);printk("EIP: %8x CS: %4x EFLAGS: %8x\n\r",esp[0],esp[1],esp[2]);
}void do_debug(long esp, long error_code)
{die("debug",esp,error_code);
}
...
大部分do_xxx又调用了die打印一些错误信息。
//取段seg中地址addr处的一个字节
#define get_seg_byte(seg,addr) ({ \
register char __res; \
__asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \:"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})
//取段seg中地址addr处的一个长字(4字节)
#define get_seg_long(seg,addr) ({ \
register unsigned long __res; \
__asm__("push %%fs;mov %%ax,%%fs;movl %%fs:%2,%%eax;pop %%fs" \:"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})
//取fs段寄存器的值(选择符)
#define _fs() ({ \
register unsigned short __res; \
__asm__("mov %%fs,%%ax":"=a" (__res):); \
__res;})
/*打印 出错中断的名称、出错号、
调用程序的EIP、EFLAGS、ESP、fs段寄存器值、段的基址、段的长度、
进程号pid、任务号、10字节指令码。
如果堆栈在用户段,则还打印16字节的堆栈内容。*/
static void die(char * str,long esp_ptr,long nr)
{long * esp = (long *) esp_ptr;int i;printk("%s: %04x\n\r",str,nr&0xffff);printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n",esp[1],esp[0],esp[2],esp[4],esp[3]);printk("fs: %04x\n",_fs());printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17));if (esp[4] == 0x17) {printk("Stack: ");for (i=0;i<4;i++)printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));printk("\n");}str(i); //取当前运行任务的任务号(include/linux/sched.h)。printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i);for(i=0;i<10;i++)printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));printk("\n\r");do_exit(11); //程序退出处理。(kernel/exit.c)
}
系统调用
什么是系统调用?
系统调用(System Call)是用户空间程序请求内核服务的一种机制。例如:
- 打开文件(
open()
) - 读写文件(
read()
/write()
) - 创建进程(
fork()
) - 退出进程(
exit()
) - 内存分配(
brk()
)
在 Linux 0.11 中,系统调用通过 中断 0x80 实现,是用户程序进入内核的唯一合法途径。
系统调用实现机制
1. 中断门设置(IDT)
在系统启动时,Linux 0.11 初始化了中断描述符表(IDT),将中断 0x80 设置为系统调用入口。
- 中断号 0x80 对应的处理函数是
system_call
。 - 该中断门允许用户态程序通过
int $0x80
指令进入内核。
2. 用户态调用系统调用
用户程序通过如下方式调用系统调用(以 fork()
为例):
#include <unistd.h>pid_t pid = fork();
编译后,该调用会生成如下汇编代码:
movl $2, %eax # fork 的系统调用号是 2
int $0x80 # 触发中断,进入内核
3. 内核处理系统调用
进入内核后,执行 system_call
函数(在 kernel/system_call.s
中定义):
主要流程
- 保存寄存器上下文(如 eax、ebx、ecx、edx、esi、edi、ebp)。
- 获取系统调用号(
eax
)。 - 检查调用号是否合法。
- 调用对应的系统调用处理函数(通过
sys_call_table
表)。 - 恢复寄存器上下文。
- 返回用户空间(
iret
)。
系统调用表:sys_call_table
Linux 0.11 使用一个函数指针数组来保存所有系统调用的入口:
fn_ptr sys_call_table[] = {sys_setup, // 0sys_exit, // 1sys_fork, // 2sys_read, // 3sys_write, // 4sys_open, // 5sys_close, // 6...
};
- 每个系统调用都有一个唯一的编号(0 ~ 128)。
- 用户程序通过
eax
寄存器传入调用号。 - 内核根据调用号从
sys_call_table
中找到对应的函数。
系统调用的函数命名规则
Linux 0.11 中,系统调用的实现函数通常以 sys_
开头,例如:
用户调用 | 内核函数 | 说明 |
---|---|---|
fork() | sys_fork() | 创建新进程 |
read() | sys_read() | 读取文件 |
write() | sys_write() | 写入文件 |
open() | sys_open() | 打开文件 |
close() | sys_close() | 关闭文件 |
exit() | sys_exit() | 进程退出 |
这些函数通常定义在 kernel/sys.c
、kernel/fork.c
、fs/open.c
、fs/read_write.c
等文件中。
系统调用的参数传递
Linux 0.11 使用寄存器传递参数,最多支持 5 个参数:
参数位置 | 寄存器 |
---|---|
第 1 个参数 | ebx |
第 2 个参数 | ecx |
第 3 个参数 | edx |
第 4 个参数 | esi |
第 5 个参数 | edi |
例如:
ssize_t read(int fd, void *buf, size_t count);
对应系统调用号为 3(sys_read
),其参数传递如下:
movl $3, %eax
movl fd, %ebx
movl buf, %ecx
movl count, %edx
int $0x80
系统调用返回值
- 系统调用的返回值通过
eax
寄存器返回给用户程序。 - 如果出错,返回值为负数(如
-EINVAL
),用户程序可以通过errno
获取错误码。
系统调用与中断的关系
机制 | 说明 |
---|---|
int 0x80 | 用户态进入内核的标准方式 |
system_call | 中断处理入口函数 |
save_all | 保存寄存器上下文 |
sys_call_table | 查找对应系统调用函数 |
restore_all | 恢复寄存器上下文 |
iret | 返回用户空间 |
系统调用的局限性
限制 | 说明 |
---|---|
支持系统调用数有限 | 最多 128 个 |
参数传递方式固定 | 使用寄存器,最多 5 个参数 |
无系统调用封装库 | 用户需手动使用 int 0x80 |
无动态注册机制 | 系统调用需静态添加到 sys_call_table |
无现代系统调用接口 | 如 mmap 、poll 、epoll 等尚未出现 |
总结:系统调用关键点
模块 | 内容 |
---|---|
系统调用号 | 存放在 eax 寄存器中 |
触发方式 | int $0x80 |
入口函数 | system_call |
参数传递 | ebx, ecx, edx, esi, edi |
返回值 | 通过 eax 返回 |
调用表 | sys_call_table 数组 |
调用函数 | sys_xxx() 函数 |
用户接口 | <unistd.h> 中定义宏 |
错误处理 | 返回负数错误码,用户用 errno 获取 |
任务调度
Linux 0.11 是一个早期的 Linux 内核版本(1991 年发布),虽然功能简单,但已经实现了多任务调度、进程管理、系统调用、中断处理、内存管理等操作系统核心机制。
任务调度概述
Linux 0.11 的任务调度器是基于时间片轮转的抢占式调度器,采用固定优先级 + 时间片递减的方式选择下一个运行的进程。
- 每个进程有一个
counter
字段表示剩余时间片。 - 时间片用完后进程被挂起,等待重新分配。
- 调度器在
schedule()
函数中实现。 - 支持最多 64 个进程(NR_TASKS = 64)。
进程状态与调度关系
Linux 0.11 中的进程状态决定了调度器是否可以调度该进程:
状态 | 宏定义 | 说明 |
---|---|---|
可运行 | TASK_RUNNING | 可以被调度运行 |
可中断睡眠 | TASK_INTERRUPTIBLE | 等待资源,可被信号唤醒 |
不可中断睡眠 | TASK_UNINTERRUPTIBLE | 等待资源,不能被信号唤醒 |
僵尸状态 | TASK_ZOMBIE | 进程已结束,等待父进程回收 |
停止状态 | TASK_STOPPED | 被调试器暂停 |
📌 只有处于 TASK_RUNNING
状态的进程才会被调度器选中。
调度器核心函数:schedule()
这是调度器核心函数,位于 sched.c
中。
调度流程
-
遍历所有进程,处理定时器和信号唤醒:
- 检查是否有进程的
alarm
时间到,发送SIGALRM
信号。 - 如果进程处于
TASK_INTERRUPTIBLE
状态,并且有未被屏蔽的信号到来,则将其状态改为TASK_RUNNING
。
- 检查是否有进程的
-
找出当前时间片最多的可运行进程:
- 遍历所有进程,找出
state == TASK_RUNNING
且counter > 0
的进程。 - 选出
counter
最大的那个进程作为下一个要运行的进程。
- 遍历所有进程,找出
-
如果所有进程的时间片都用完了:
- 对所有进程重新分配时间片:
counter = (counter >> 1) + priority;
counter >> 1
:保留原来的一半时间片。+ priority
:加上进程的优先级,保证交互式进程获得更多运行机会。
- 对所有进程重新分配时间片:
-
调用
switch_to(next)
切换到选中的进程。
调度器特点总结
特性 | 描述 |
---|---|
调度策略 | 时间片轮转 + 优先级 |
时间片机制 | 每次调度递减,用完后重新分配 |
支持最大进程数 | 64 个(NR_TASKS) |
支持优先级 | 每个进程有 priority 字段 |
抢占机制 | 时间片用完即切换,不支持实时抢占 |
无写时复制(COW) | fork() 完全复制父进程页表 |
无动态优先级调整 | 优先级固定 |
无组调度 | 不支持进程组调度 |
无调度类 | 没有现代 Linux 的调度类(如 CFS、RT、DL) |
进程结构体 task_struct
中与调度相关的字段
struct task_struct {long state; // 进程状态(TASK_RUNNING 等)long counter; // 当前时间片long priority; // 优先级long signal; // 信号位图struct sigaction sigaction[32]; // 信号处理函数...
};
关键字段解释
state
:当前进程状态,决定是否被调度。counter
:剩余时间片,调度器选择依据。priority
:进程优先级,影响时间片重新分配。signal
:信号位图,影响进程唤醒。
时间片分配机制
Linux 0.11 的时间片分配不是固定的,而是根据进程的历史使用情况动态调整:
counter = (counter >> 1) + priority;
counter >> 1
:保留原来时间片的一半,避免长时间得不到运行的进程完全失去时间片。+ priority
:优先级越高,分配的时间片越多。
📌 这种机制虽然简单,但能保证交互式进程(如 shell、终端)获得较多运行时间。
调度触发时机
调度器在以下几种情况下被调用:
触发时机 | 说明 |
---|---|
schedule() 被显式调用 | 如进程主动放弃 CPU(如调用 pause() ) |
time_interrupt() 中断 | 每次时钟中断会减少当前进程时间片,若为 0 则调用 schedule() |
sys_waitpid() 等系统调用 | 等待子进程结束时主动调度 |
sys_exit() | 进程退出时调用调度器 |
sys_pause() | 进程进入等待状态,调用调度器 |
调度器的局限性
虽然 Linux 0.11 的调度器实现了基本的多任务调度功能,但也存在以下明显限制:
限制 | 说明 |
---|---|
无实时调度类 | 无法满足实时系统需求 |
无调度组 | 不支持进程组调度 |
无动态优先级 | 所有进程优先级固定 |
无公平调度 | 不保证每个进程获得公平 CPU 时间 |
无负载均衡 | 不适用于 SMP 多核系统 |
时间片分配粗糙 | 只有 64 个进程,不适用于现代系统 |
进程创建与调度关系(fork()
)
Linux 0.11 的 fork()
系统调用会复制父进程的地址空间、寄存器状态、页表等信息,创建一个新进程。
int sys_fork(long ebx, long ecx, long edx, ...)
- 子进程初始状态为
TASK_RUNNING
。 - 子进程初始时间片为父进程的一半。
- 父子进程共享代码段,数据段和堆栈段复制(无写时复制)。
📌 fork()
后,调度器有机会在下一次调度中选择新进程运行。
总结: 任务调度关键点
模块 | 关键点 |
---|---|
调度器类型 | 基于时间片轮转的抢占式调度器 |
调度函数 | schedule() |
进程状态 | TASK_RUNNING 才能被调度 |
时间片机制 | 每次调度递减,用完后重新分配 |
优先级机制 | 影响时间片分配,但不参与调度选择 |
调度触发时机 | 时钟中断、系统调用、进程退出等 |
进程创建 | fork() 创建新进程,父子共享代码段 |
调度目标 | 在有限资源下实现多任务并发执行 |
局限性 | 不适用于现代 SMP、实时系统,调度公平性差 |