Linux内核重点精要

本文详细探讨了Linux内核中的核心概念,包括进程与线程、进程调度、系统调用、中断处理、内存管理和虚拟文件系统。重点介绍了进程的创建与上下文、调度器的工作原理、中断处理流程、系统调用的实现以及内存管理中的页缓存与回写策略。此外,还涉及到了模块管理和内核中断处理函数结束后的任务,如软中断、tasklet和工作队列的应用。
摘要由CSDN通过智能技术生成

- 内核版本 Linux Kernel 2.6.34, 与 Robert.Love的《Linux Kernel Development》(第三版)所讲述的内核版本一样

- 源代码下载路径: https://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.34.tar.bz2

 

1. 进程与线程

1) 进程(process)与线程(thread)在Linux内核中都是通过 task_struct对象来描述的。也就是说,只有站在用户态(user space)的视角,才区分进程和线程。在内核态(Kernel space),不管是进程还是线程,都是一个 task_struct 的对象实例。其大致关系如图1所示


Figure1进程、线程与task_struct

1)    用户态创建进程使用的是sys_fork()或者sys_vfork()系统调用,创建线程(POSIX线程库的pthread_create()函数,需要链接到libpthread.so库)使用sys_clone()系统调用会多一些,。虽然用的系统调用API不同,但是sys_fork()、sys_vfork()、sys_clone()这三个系统调用在内核态中都会使用do_fork()函数来执行实际的进程(线程)创建流程,do_fork()最终会在内核中都会创建一个task_struct对象实例,但是由于进程与线程API传入参数不同,因而创建的不同task_struct实例所维护的资源以及资源的权限是不同的。相关系统调用的代码如下:

注意到,一般pthread_create()调用sys_clone()创建线程时,是可以设置新的线程栈的(newsp表示new stack pionter)

 

int sys_fork(struct pt_regs *regs)
{
	return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}

int sys_vfork(struct pt_regs *regs)
{
	return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,
		       NULL, NULL);
}

long
sys_clone(unsigned long clone_flags, unsigned long newsp,
	  void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
{
	if (!newsp)
		newsp = regs->sp;
	return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
}


3) 一个task_struct是一个程序在操作系统运行的基本单位,也可以说是一个cpu调度的基本单位,一个task_struct可以在某时刻占有某一个CPU。

4) 一个task_struct所维护的资源包括虚拟地址空间,文件系统信息、文件描述符、信号状态与信号处理函数等,具体参考Linux 内核源码中 include/linux/sched.h 文件中task_struct对象的声明。

5) 内核线程

- 只运行在内核态的线程

- 内核线程也是用task_struct对象来描述

- 内核线程没有独立的虚拟地址空间,不能切换到用户态运行。

6) 进程上下文(context)与中断上下文

- 不管是当前CPU执行的是用户态还是内核态程序,只要该运行的程序和进程关联,我们就说当前程序运行在进程上下文

- 与进程上下文对立的,当前CPU如果执行的是中断处理程序,那么就和进程无关,我们就说当前程序运行在中断上下文

7)  fork() 创建进程的执行机制

- 在当前进程中执行fork(),会进入内核态,创建一个新的task_struct,进而创建出新的进程

- 新的进程是调用fork()的原进程的子进程

- 写时拷贝机制。新进程刚创建时,与父进程共享资源,同一个虚拟地址空间,只有新建子进程有发出写的命令时,Linux系统会产生缺页的exception,进而将新进程相关的数据资源拷贝到新的子进程的虚拟地址空间。

8)  task_struct的存放与查找

- 内核可以通过current宏来找到当前进程的task_struct对象

- 不同体系结构的current宏实现方法不同,有的体系结构有硬件寄存器存放task_struct对象指针。

- 旧的X86架构没有存放task_struct对象指针的寄存器,通过在进程内核栈的尾端保存thread_info对象指针-- >在内核堆栈尾地址找到thread_info对象指针-- > 通过thread_info->task来找到task_struct对象指针。

 

2. 进程调度

1) 进程的主要状态

- TASK_RUNNING, 可执行状态。 进程此时可能已经获得CPU正在执行,也可能尚未获取CPU资源。如果处于TASK_RUNNING状态下的进程没有获取CPU,则该进程是可以被调度的,需要排队,按照调度器制定的规则,等候获取CPU资源。

 

- TASK_INTERRUPTIBLE, 被阻塞处于睡眠状态,此时不能被调度器调度, 既可以被信号唤醒,也可以被其它进程主动唤醒,变成可调度的TASK_RUNNING状态。


- TASK_UNINTERRUPTIBLE, 被阻塞处于睡眠状态,不能被信号唤醒,只能被其它进程主动唤醒,才能恢复到可调度的TASK_RUNNING状态。


- __TASK_STOPPED, 停止状态,没有投入运行,也不能被投入运行


- __TASK_TRACED, 被其它进程跟踪状态,一般用在ptrace系统调用中,专门用来检查程序运行轨迹,以便debug。


- EXIT_ZOMBIE, 僵尸进程,只进程被kill掉之后,其获取的资源没有被父进程及时回收,处于僵尸状态。


- EXIT_DEAD, 死亡的进程

 

2) 调度器

- 调度器是一个对象,该对象通过一套规则来选择一个在在调度队列中的TASK_RUNNING状态的进程投入CPU运行


- 调度器类, 内核的include/linux/sched.h 文件中声明了,struct sched_class, 这是一个调度器的基类,开发者可以通过实现该sched_class对应的函数方法,从而定制自己的调度器。


- Linux 内核中可以拥有多种调度器,以实现不同的调度策略。进程、调度器与CPU的关系如图2所示。

          

Figure2  进程,调度器与CPU的关系



          

3)   Linux 内核中已经实现的调度器

-   完全公平调度器(CFS)

代码在kernel/sched_fair.c中实现。不同于Unix系统的进程调度,CFS调度器没有时间片的概念。而是采用一种加权虚拟实时的方式对进程运行的时间进行记账(task_struct.se.vruntime变量的值)。所以的进程task_struct对象通过vruntime排序,插入等待调度的进程队列,该队列采用红黑树进行管理和查找,使得查找效率最高(时间复杂度log(N))。CFS调度器会从调度队列的红黑树中选择vruntime最小的进程(红黑树最左边的节点)投入运行。

CFS是非实时的调度策略,用户态通过SCHED_NORMAL的宏设置该调度策略。

-  实时调度器(RT)

代码在kernel/sched_rt.c中实现。Linux内核的实时调度器不是硬实时的,而是一种软实时的调度方法。即内核尽力保证进程在它限定时间之前运行,但不保证总能满足这些进程的要求。

实时调度可以设置SCHED_FIFO和 SCHED_RR两种调度策略。实时进程对应的优先级范围0-99,数值越大,优先级越高。

实时调度器的优先级高于非实时调度器。

-   Idle Task 调度器

代码在kernel/sched_idletask.c中实现。优先级最低的调度器,顾名思义,用于管理空闲进程。

 

4)   上下文切换与抢占

-  调度器通过算法选择下一个投入CPU运行的进程后,需要通过上下文切换函数,切换CPU中运行的进程。Kernel/sched.c 中context_switch()函数用于实现上下文的切换。

 

-  用户抢占。当进程从内核态返回(包括中断返回)用户态时,如果need_resched标志被置位,则内核会选择一个更适合的进程投入运行。

 

 

-   内核抢占。Linux支持运行在内核态的进程被抢占,只要进程的thread_info. preempt_count 为0时,即没有加锁,那么内核态的进程就是可以抢占的。内核抢占可以发生在中断返回时,锁全部释放掉(再次具备可抢占性),显示调用schedule(),被阻塞时。


3. 系统调用

 

1)     系统调用是用户态与内核态程序交互通信的一组接口,  该接口限定了用户态程序访问内核的机制,为系统稳定性提供保障。

 

2)     程序员使用系统调用访问Linux内核的一般流程:应用程序  -- > C 程序库 -- > 系统级POSIX 标准API函数-- > 系统调用-- > 内核代码

 

3)     系统调用的实现。

 

-         在X86系统中,系统调用到内核代码调用,实现过程是通过int $0x80指令产生的软中断(中断号128)异常,在异常代码处理中通过系统调用号,查找相应的内核函数来实现的。

-         X86 系统处理该异常的代码和体系结构有关,一般在arch/x86/kernel/entry_32.S(entry_64.S)中实现。

-         不同的系统调用参数不同(例如open和mmap)。不同个数参数的系统调用函数在内核中SYSCALL_DEFINEX来定义(例如: SYSCALL_DEFINE3(chown,const char __user *, filename, uid_t, user, gid_t, group)),系统调用函数的参数一般通过寄存器的方式进行传递,

 

4)     系统调用号。

-         每个Linux系统调用都有一个系统调用号。

-         一般系统调用的函数都放在sys_call_table的数组中,内核通过用户态传入的系统调用号,在sys_call_table中查找内核中具体的系统调用函数。

5)     Linux内核X86体系结构现有的系统调用

-         Linux内核现有的系统调用表sys_call_table在 arch/x86/kernel/syscall_table_32.S中定义, 每个系统调用号对应着该

  • 8
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值