Linux内核学习系列(1)——系统调用

前言

由于工作需要,个人从java栈转为了c语言栈,并需要深入学习linux内核。本系列记录一些个人学习笔记。由于Linux内核涉及内容以及知识点很多,一开始接触十分痛苦,通过反复阅读《Linux内核完全注释》一书才逐渐能够看懂源码。在理解的过程中,个人发现自上而下地探索内核,才是最适合自己的学习内核的方式。因此,本系列主要从自上而下的角度,进行笔记记录。整个系列配图及概念描述将直接引用《Linux内核完全注释》中的内容。

系统调用

本系列第一篇从系统调用开始,因为在学习的过程中,个人发现系统调用能够将各个知识点串起来,也正好借此机会对之前学习的知识点进行梳理。

什么是系统调用

系统调用(通常称为 syscalls)是 Linux 内核与上层应用程序进行交互通信的唯一接口。从对中断机制的说明可知,用户程序通过直接或间接(通过库函数)调用中断 int 0x80,并在 eax寄存器中指定系统调用功能号,即可使用内核资源,包括系统硬件资源。不过通常应用程序都是使用具有标准接口定义的 C 函数库中的函数间接地使用内核的系统调用,见下图 所示。

Linux内核学习系列(1)——系统调用_第1张图片
为了更好地理解系统调用,需要先了解中断。因此阅读时,先跳至中断章节进行阅读。

系统调用实现

在了解了中断机制及中断执行过程后,我们可以想到,只要让system_call实现类似中断机制的执行过程,就能够完成上述概念提及的功能

因此,system_call的目的是根据eax中的编号,调用系统调用表中对应的某个程序。如其代码所描述的:kernerl/system_call.s

_system_call:
	cmpl $nr_system_calls-1,%eax
	ja bad_sys_call
	push %ds
	push %es
	push %fs
	pushl %edx
	pushl %ecx		# push %ebx,%ecx,%edx as parameters
	pushl %ebx		# to the system call
	movl $0x10,%edx		# set up ds,es to kernel space
	mov %dx,%ds
	mov %dx,%es
	movl $0x17,%edx		# fs points to local data space
	mov %dx,%fs
	call _sys_call_table(,%eax,4)
	pushl %eax
	movl _current,%eax
	cmpl $0,state(%eax)		# state
	jne reschedule
	cmpl $0,counter(%eax)		# counter
	je reschedule

对应的表为_sys_call_table,其在linux\sys.h中被定义

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 };

中断

什么是中断

中断(Interrupt)和异常(Exception)是指明系统、处理器或当前执行程序(或任务)的某处出现一个事件,该事件需要处理器进行处理。通常,这种事件会导致执行控制被强迫从当前运行程序转移到被称为中断处理程序(interrupt handler)或异常处理程序(exception handler)的特殊软件函数或任务中。处理器响应中断或异常所采取的行动被称为中断/异常服务(处理)。通常,中断发生在程序执行的随机时刻,以响应硬件发出的信号。系统硬件使用中断来处理外部事件,例如要求为外部设备提供服务。当然,软件也能通过执行 INT n 指令产生中断。

简单地说,中断可以暂停当前CPU所执行的程序,转而执行中断程序。以时钟中断为例,为了让内核具备时间观念,可以通过时钟芯片隔n毫秒产生一次时钟中断,当时钟中断发生时,CPU必须停下手中的活,转而执行时钟中断程序。如此一来,相当于内核能够隔n毫秒,定期地执行一些时间。

再比如,上述的系统调用也是一种中断。意味着当进程执行系统调用时,会产生中断,使得CPU立刻执行系统调用程序。因此,中断能够让CPU立刻执行一些事情,具体什么事情,与中断程序有关。

如何实现中断

由上可知,中断需要实现的功能很简单,打断CPU,并且能够让CPU跳转执行中断程序。具体地,中断可以分为由硬件发出的中断,以及程序主动发出的中断。例如,当用户敲击键盘时,键盘会直接触发一个中断,CPU能够立刻处理输入的键盘内容。当程序执行到系统调用语句时,也会触发中断。那么如何设计,才能够十分简洁地实现多个中断指哪断哪的功能呢?

很容易可以有如下实现:

  1. 设计一个表,表中每一项存放不同中断执行程序的地址
  2. 设计一个中断触发函数,需要触发中断时,传入一个编号,作为表的索引
  3. 根据编号找出表中对应项,获得中断执行程序地址
  4. CPU跳转到该中断执行程序地址执行

对应地,CPU中实现也是类似

  1. 设计一个表,称为中断描述符表(IDT),表中每一项存放不同中断执行程序的段选择符
  2. 设计一个中断触发函数 INT,通过INT N触发中断,传入编号N,N成为中断向量
  3. 根据中断向量,找出IDT中对应表项,获得段选择符
  4. CPU根据段选择符,获得段描述符在全局描述符表(GDT)中的索引
  5. 通过段选择符索引,从GDT中找出对应段描述符
  6. 段描述符中,记录了中断执行程序的地址
  7. CPU跳转到该中断执行程序地址执行

整个过程如下图所示
Linux内核学习系列(1)——系统调用_第2张图片
相较于最初的设计,CPU实现上引入了全局描述符表GDT,让整个过程多了一步。那么为什么要这么做呢?这就需要了解CPU的保护模式以及分段机制了

保护模式与分段机制

上节通过中断过程,引出了CPU在保护模式下的寻址方式,并采用分段机制。在此简单描述一下CPU的寻址方式(不考虑分页机制)。CPU寻址方式在实模式下及保护模式下各有不同。

在实模式下,段寄存器直接存储段基址,线性地址=段基址左移4位+偏移量

在保护模式下,段寄存器存储段选择符,线性地址的计算,需要先根据段选择符找出GDT表中对应的段描述符,根据段描述符获得段基址,线性地址=段基址+偏移量。而这种方式就称为分段机制

对比上述寻址方式,可以发现,尽管分段机制寻址方式变得复杂,但能够通过描述符其他字段存储一些段属性,如权限等。从而可以根据属性进行权限控制,完成保护。

分段机制寻址如下图所示
Linux内核学习系列(1)——系统调用_第3张图片
由上可知分段机制的必要性。当然,为了加速查找,段寄存器中除了存储段选择符,还会直接缓存段基址,这部分就不展开。以及GDT与LDT的关系等内容,也不展开。《Linux内核完全注释》中都有详细说明。

实现中断

至此,中断的大致原理已经基本了解,可以得出中断机制的实现要素:

  1. 初始化IDT、GDT,并根据目标程序段基址填充表的内容
  2. 将IDT、GDT地址分别写入IDTR、GDTR寄存器中,否则CPU不清楚去哪找到这两个表
  3. 编写对应中断执行程序
  4. 调用INT N。注意,N是IDT的索引,不是具体地段描述符
  5. CPU根据前述过程,跳转至中断执行程序并执行

具体地,可以从汇编代码中对上述过程进行印证。(linux-0.11)

初始化IDT、GDT,并根据目标程序段基址填充表的内容:boot/head.S

_idt:	.fill 256,8,0		# idt is uninitialized

_gdt:	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	/* 16Mb */
	.quad 0x00c0920000000fff	/* 16Mb */
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */

0x00c09a0000000fff 根据段描述符进行拆分,表示段基址为 0x00000,段限长为 0x0fff

此处idt只是填充0占位,linux会进行进一步初始化。例如,kernel/sched.c中sched_init通过set_system_gate(0x80,&system_call)将system_call的地址写入idt的0x80中

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
	"movw %0,%%dx\n\t" \
	"movl %%eax,%1\n\t" \
	"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)
//idt就是中断描述表,这里就是往表里填中断号和中断程序的地址
#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)

将IDT、GDT地址分别写入IDTR、GDTR寄存器中,否则CPU不清楚去哪找到这两个表:boot/head.S

setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea _idt,%edi
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)
	addl $8,%edi
	dec %ecx
	jne rp_sidt
	lidt idt_descr
	ret

/*
 *  setup_gdt
 *
 *  This routines sets up a new gdt and loads it.
 *  Only two entries are currently built, the same
 *  ones that were built in init.s. The routine
 *  is VERY complicated at two whole lines, so this
 *  rather long comment is certainly needed :-).
 *  This routine will beoverwritten by the page tables.
 */
setup_gdt:
	lgdt gdt_descr
	ret

lgdt gdt_descr : 将gdt_descr地址写入gdtr
lidt idt_descr :将idt_descr地址写入idtr

编写对应中断执行程序(以system_call为例):kernel/system_call.s

_system_call:
	cmpl $nr_system_calls-1,%eax
	ja bad_sys_call
	push %ds
	push %es
	push %fs
	pushl %edx
	pushl %ecx		# push %ebx,%ecx,%edx as parameters
	pushl %ebx		# to the system call
	movl $0x10,%edx		# set up ds,es to kernel space
	mov %dx,%ds
	mov %dx,%es
	movl $0x17,%edx		# fs points to local data space
	mov %dx,%fs
	call _sys_call_table(,%eax,4)
	pushl %eax
	movl _current,%eax
	cmpl $0,state(%eax)		# state
	jne reschedule
	cmpl $0,counter(%eax)		# counter
	je reschedule

你可能感兴趣的:(内核,linux)