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

【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)
名称类型typeDPL用途
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,&divide_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,&parallel_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 中定义):

主要流程
  1. 保存寄存器上下文(如 eax、ebx、ecx、edx、esi、edi、ebp)。
  2. 获取系统调用号(eax)。
  3. 检查调用号是否合法。
  4. 调用对应的系统调用处理函数(通过 sys_call_table 表)。
  5. 恢复寄存器上下文。
  6. 返回用户空间(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.ckernel/fork.cfs/open.cfs/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
无现代系统调用接口mmappollepoll 等尚未出现

总结:系统调用关键点

模块内容
系统调用号存放在 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 中。

调度流程

  1. 遍历所有进程,处理定时器和信号唤醒

    • 检查是否有进程的 alarm 时间到,发送 SIGALRM 信号。
    • 如果进程处于 TASK_INTERRUPTIBLE 状态,并且有未被屏蔽的信号到来,则将其状态改为 TASK_RUNNING
  2. 找出当前时间片最多的可运行进程

    • 遍历所有进程,找出 state == TASK_RUNNINGcounter > 0 的进程。
    • 选出 counter 最大的那个进程作为下一个要运行的进程。
  3. 如果所有进程的时间片都用完了

    • 对所有进程重新分配时间片:
      counter = (counter >> 1) + priority;
      
      • counter >> 1:保留原来的一半时间片。
      • + priority:加上进程的优先级,保证交互式进程获得更多运行机会。
  4. 调用 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、实时系统,调度公平性差
http://www.dtcms.com/a/286947.html

相关文章:

  • 2025年6月GESP(C++二级): 幂和数
  • 游戏盾能否保护业务免受DDoS攻击吗?
  • Django母婴商城项目实践(五)- 数据模型的搭建
  • 【Python练习】 049. 编写一个函数,实现简单的文本编辑器功能,支持增删改查
  • 你的品牌需要一个AI首席内容官——解构BrandCraft如何解决内容创作的终极痛点
  • 枚举算法入门
  • 【2025/07/18】GitHub 今日热门项目
  • 北斗网格位置码详解:经纬度到二维网格码的转换(非极地)
  • 针对BERT模型的理解
  • 04-三思而后行:解锁AI的“内心戏”
  • VMware安装Win10教程(附安装包)虚拟机下载详细安装图文教程
  • chainlink VRF中文教程(含mock),解决error: Arithmetic Underflow in createSubscription
  • bmp图像操作:bmp图像保存及raw与bmp转换
  • 二分答案之第 K 小/大
  • CMake指令:常见内置命令行工具( CMake -E )
  • 乙烯丙烯酸酯橡胶市场报告:性能优势、行业现状与发展前景​
  • selenium后续!!
  • 【数据集】1970-2023年全球温室气体排放 GHG 数据集 EDGAR
  • 语音直播和视频直播的测试要点
  • 【ROS1】06-ROS通信机制——话题通信
  • OOA、OOD 与 OOP:面向对象范式的核心支柱详解
  • 接口测试的原则、用例与流程详解
  • ModelSim 配合 Makefile 搭建 Verilog 仿真工程
  • Docker-下载和安装
  • ADVB协议内容分析
  • LeetCode Hot100【6. Z 字形变换】
  • GI6E 加密GRID電碼通信SHELLCODE載入
  • CCF编程能力等级认证GESP—C++3级—20250628
  • 操作系统-处理机调度和死锁进程同步
  • 基于Qwen2.5-3B-Instruct的LoRA微调与推理实战指南