linux kernel总结

    xiaoxiao2021-11-29  25

    内核与操作系统

    内核是操作系统的核心部分,包含了系统运行的核心过程,决定系统的性能,操作系统启动内核被装入到RAM中; 操作系统与底层硬件设备交互和为运行应用程序提供执行环境。

    Linux内核与微内核比较:

    微内核:内核只需要一个很小的函数集,通常包括几个同步原语,一个简单的调度程序和进程间通信机制。 运行在微内核之上的几个系统进程实现系统级功能:内存分配,设备驱动程序……完全的模块化进程。任何操作系统层都是独立的程序模块,通过模块化的方法定义明确清晰的软件接口与其它层交互。内核中暂且不需要执行的系统进程可以被调出或者撤销。微内核便于移植和充分利用RAM,但开销大效率是个问题。

    宏内核:Linux内核:单块结构。内核的全部代码,包括所有子系统(如内存管理、文件系统、设备驱动程序)都打包到一个文件中。内核中的每个函数都可以访问内核中所有其他部分。模块特性依赖于内核与用户层之间设计精巧的通信方法,这使得模块的热插拔和动态装载得以实现。每个内核层都被继承到整个内核程序中,并代表着当前进程在内核态下运行。

    模块化(非进程)——允许在运行状态下动态的安装。模块是一个目标文件,其代码在运行时链接到内核或从内核解除链接。目标代码通常是一组函数组成,用来实现文件系统,驱动程序……这些模块与其他静态链接内核函数一样,代表着当前进程在内核态下执行,直接函数调用避免进程切换消息传递的开销,效率可能更高。

    Linux用户程序两种状态: 用户态和内核态;用户态切换到内核态:

    l 进程系统调用 l CPU异常 l 中断 l 内核线程被执行

    多用户系统: 能并发执行和独立的执行多个用户的应用程序,各个用户拥有独立空间。用户组,Root用户。

    Linux进程: 进程Process:

    l 操作系统的基本抽象。 l 进程是程序执行时的一个实例;一个运行程序的执行上下文。 l 几个进程能并发的执行同一个程序;而同一个进程能顺序执行几个程序。 l 具有独立的地址空间;多个进程可以同时执行。

    进程受内核管理;每个进程由一个进程描述符表示,包含进程当前的状态信息。

    当内核暂停一个进程的执行时,就把几个相关处理器寄存器的内容保存在进程描述符中。这些寄存器包括:

        l 程序计数器PC和栈指针SP寄存器     l 通用寄存器     l 浮点寄存器     l 包含CPU状态信息的处理控制寄存器     l 跟踪进程对RAM访问的内存管理寄存器

      当内核恢复执行进程时:将进程描述符中合适字段来装在CPU寄存器,根据程序计数器指向恢复到程序执行的地方。 Linux重入内核: 内核可重入:

    可重入函数:使用局部变量 实现同步机制:信号量、锁、关中断 进程执行状态切换: 进程在用户态与内核态的转换,Linux是抢占式内核 进程地址空间:每个进程运行在似有地址空间 同步和临界区:内核数据操作访问。 进程间通信IPC:信号量、消息队列、共享内存 进程管理:fork与_exit,exec(),子进程与父进程

    Linux文件系统: 文件系统是对存储设备上的数据和元数据进行组织的机制,以树形结构组织。 文件类型:

    不同文件 目录 符号链接 面向块得设备文件 (设备驱动相关) 面向字符的设备文件 (设备驱动相关) 管道(pipe)和命名管道(named pipe)(进程间通信相关) 套接字(socket) (进程间通信相关)

    文件访问权限和访问模式 文件描述符和索引节点:记录文件的信息数据。 文件操作的系统调用:open、read、write…… 内存管理:

    虚拟内存:处于应用程序内存请求与硬件内存单元之间的逻辑层。 随即访问存储器RAM:一部分用于内核映像,其余虚拟内存处理 内核内存分配器:KMA 处理内存请求子系统     l 速度快     l 减少内存浪费     l 减轻内存碎片     l 与其他内存管理合作(页框)     l 内存分配算法

    进程虚拟空间地址处理:内核分配给进程的虚拟地址空间由以下内存区组成:

        l 程序的可执行代码     l 程序的初始化数据     l 程序未初始化数据     l 初始化程序栈     l 所需共享库的可执行代码和数据     l 程序动态请求的内存堆

      高速缓存:

    设备驱动程序: 内核通过设备驱动程序与I/O设备交互,设备驱动程序在内核中,用户程序通过内核访问设备。

    进程

    一 进程与线程

    进程就是处于执行期的程序,包含了独立地址空间,多个执行线程等资源。 线程是进程中活动的对象,每个线程都拥有独立的程序计数器、进程栈和一组进程寄存器。 内核调度的对象是线程而不是进程。对Linux而言,线程是特殊的进程。

    二 进程描述符及任务结构

    内核使用双向循环链表的任务队列来存放进程,使用结构体task_struct来描述进程所有信息。

    1 进程描述符task_struct struct task_struct {}结构体相当大,大约1.7K字节。大概列出一些看看:

    struct task_struct { //这个是进程的运行时状态,-1代表不可运行,0代表可运行,>0代表已停止。 volatile long state; /* flags是进程当前的状态标志,具体的如: 0x00000002表示进程正在被创建; 0x00000004表示进程正准备退出; 0x00000040 表示此进程被fork出,但是并没有执行exec; 0x00000400表示此进程由于其他进程发送相关信号而被杀死 。 */ unsigned int flags; //表示此进程的运行优先级 unsigned int rt_priority; //这里出现了list_head结构体 struct list_head tasks; //这里出现了mm_struct 结构体,该结构体记录了进程内存使用的相关情况,详情请参考 struct mm_struct *mm; /* 接下来是进程的一些状态参数*/ int exit_state; int exit_code, exit_signal; //这个是进程号 pid_t pid; //这个是进程组号 pid_t tgid; //real_parent是该进程的”亲生父亲“,不管其是否被“寄养”。 struct task_struct *real_parent; //parent是该进程现在的父进程,有可能是”继父“ struct task_struct *parent; //这里children指的是该进程孩子的链表,可以得到所有孩子的进程描述符,但是需使用list_for_each和list_entry,list_entry其实直接使用了container_of,详情请参考 struct list_head children; //同理,sibling该进程兄弟的链表,也就是其父亲的所有孩子的链表。用法与children相似。 struct list_head sibling; //这个是主线程的进程描述符,也许你会奇怪,为什么线程用进程描述符表示,因为linux并没有单独实现线程的相关结构体,只是用一个进程来代替线程,然后对其做一些特殊的处理。 struct task_struct *group_leader; //这个是该进程所有线程的链表。 struct list_head thread_group; //顾名思义,这个是该进程使用cpu时间的信息,utime是在用户态下执行的时间,stime是在内核态下执行的时间。 cputime_t utime, stime; //下面的是启动的时间,只是时间基准不一样。 struct timespec start_time; struct timespec real_start_time; //comm是保存该进程名字的字符数组,长度最长为15,因为TASK_COMM_LEN为16。 char comm[TASK_COMM_LEN]; /* 文件系统信息计数*/ int link_count, total_link_count; /*该进程在特定CPU下的状态*/ struct thread_struct thread; /* 文件系统相关信息结构体*/ struct fs_struct *fs; /* 打开的文件相关信息结构体*/ struct files_struct *files; /* 信号相关信息的句柄*/ struct signal_struct *signal; struct sighand_struct *sighand; /*这些是松弛时间值,用来规定select()和poll()的超时时间,单位是纳秒nanoseconds */ unsigned long timer_slack_ns; unsigned long default_timer_slack_ns; };

    2 分配进程描述符

      当进程由于中断或系统调用从用户态转换到内核态时,进程所使用的栈也要从用户栈切换到内核栈。通过内核栈获取栈尾thread_info,就可以获取当前进程描述符task_struct。每个进程的thread_info结构在他的内核栈的尾端分配。结构中task域中存放是指向该任务实际的task_struct。 内核处理进程就是通过进程描述符task_struct结构体对象来操作。所以操作进程要获取当前正在运行的进程描述符。通过thread_info的地址就可以找到task_struct地址;在不同的体系结构上计算thread_info的偏移地址不同。

    /* linux-2.6.38.8/arch/arm/include/asm/current.h */ static inline struct task_struct *get_current(void) { return current_thread_info()->task; } ` #define current (get_current()) /* linux-2.6.38.8/arch/arm/include/asm/thread_info.h */ static inline struct thread_info *current_thread_info(void) {    //栈指针 register unsigned long sp asm ("sp"); return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); }

    3 进程的状态

    系统中的每个进程都必然处于五种进程状态中的一种或进行切换。该域的值也必为下列五种状态标志之一:

    TASK_RUNNING(运行)—进程是可执行的;它或者正在执行,或者在运行队列中等待执行(运行队列将会在第4章中讨论)。这是进程在用户空间中执行的唯一可能的状态;这种状态也可以应用到内核空间中正在执行的进程。TASK_INTERRUPTIBLE(可中断)—进程正在睡眠(也就是说它被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行。TASK_UNINTERRUPTIBLE(不可中断)—除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断状态,使用得较少。__TASK_TRACED—被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。__TASK_STOPPED(停止)—进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。

    三 进程创建

    fork:copy当前进程创建一个新的进程; exec:读取可执行文件并将其载入地址空间开始运行。

    1 fork过程 创建进程都是通过调用do_fork函数完成,其中提供了很多参数标志来表明进程创建的方式。

    long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { struct task_struct *p; …… //创建进程 p = copy_process(clone_flags, stack_start, regs, stack_size, child_tidptr, NULL, trace); …… //将进程加入到运行队列中 wake_up_new_task(p); }

    copy_process里面通过父进程创建子进程,并未执行:

    task_struct *copy_process(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *child_tidptr, struct pid *pid, int trace) { struct task_struct *p; //创建进程内核栈和进程描述符 p = dup_task_struct(current); //得到的进程与父进程内容完全一致,初始化新创建进程 …… return p; }

    dup_task_struct根据父进程创建子进程内核栈和进程描述符:

    static struct task_struct *dup_task_struct(struct task_struct *orig) { struct task_struct *tsk; struct thread_info *ti; int node = tsk_fork_get_node(orig); //创建进程描述符对象 tsk = alloc_task_struct_node(node); //创建进程内核栈 thread_info ti = alloc_thread_info_node(tsk, node); //使子进程描述符和父进程一致 err = arch_dup_task_struct(tsk, orig); //进程描述符stack指向thread_info tsk->stack = ti; //使子进程thread_info内容与父进程一致但task指向子进程task_struct setup_thread_stack(tsk, orig); return tsk; }

    创建进程copy_process之后并未执行,返回到do_fork中,将新创建进程加入到运行队列中等待被执行。

    四 线程在Linux中的实现与内核线程 线程机制提供了在同一个程序共享内存地址空间,文件等资源的一组线程。在Linux内核中把所有线程都当做进程来实现。 内核并没有提供调度算法,或者数据结构来表征线程,而是作为与其他进程共享资源的进程;与其他系统不同。

    内核线程与普通进程间的区别是: 内核线程没有独立的地址空间 只在内核空间运行,不会切换到用户空间

    五 进程终结 进程终结时内核释放其所占有的资源,并告诉父进程,更新父子关系。调用exit终结进程,进程被终结时通常最后都要调用do_exit来处理。

    void do_exit(long code) { //获取当前运行进程 struct task_struct *tsk = current; …… //sets PF_EXITING exit_signals(tsk); //释放task_struct的mm_struct内存 exit_mm(tsk); //退出接收IPC信号队列 exit_sem(tsk); //进程名字空间 exit_shm(tsk); //文件描述符 exit_files(tsk); //文件系统 exit_fs(tsk); //资源释放 exit_thread(); //向父进程发送信号 exit_notify(tsk, group_dead); …… //切换到其他进程 tsk->state = TASK_DEAD; tsk->flags |= PF_NOFREEZE; schedule(); …… }

    进程调度:

    在可运行态进程之间分配有限处理器时间资源的内核子系统。

    一 调度策略 1 进程类型 I/O消耗型进程:大部分时间用来提交I/O请求或是等待I/O请求,经常处于可运行状态,但运行时间短,等待请求过程时处于阻塞状态。如交互式程序。

    处理器消耗型进程:时间大都用在执行代码上,除非被抢占否则一直不停的运行。 综合型:既是I/O消耗型又是处理器消耗型。 调度策略要在:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)之间寻找平衡。

    2 调度概念

    优先级:基于进程价值和对处理器时间需求进行进程分级的调度。 时间片:表明进程被抢占前所能持续运行的时间,规定一个默认的时间片。时间片过长导致系统交互性的响应不好,程序并行性效果差;时间片太短增大进程切换带来的处理器耗时。矛盾! 时间片耗尽进程运行到期,暂时不可运行状态。直到所有进程时间片都耗尽,重新计算进程时间片。 Linux调度程序提高交互式程序优先级,提供较长时间片;实现动态调整优先级和时间片长度机制。 进程抢占:Linux系统是抢占式,始终运行优先级高的进程。

    3 调度算法

    可执行队列:runqueue;给定处理器上可执行进程的链表,每个处理器一个。每个可执行进程都唯一归属于一个可执行队列。

    运行队列是调度程序中最基本的数据结构:

    struct runqueue {   spinlock_t lock; /* 保护运行队列的自旋锁*/   unsigned long nr_running; /* 可运行任务数目*/  unsigned long nr_switches; /* 上下文切换数目*/   unsigned long expired_timestamp; /* 队列最后被换出时间*/   unsigned long nr_uninterruptible; /* 处于不可中断睡眠状态的任务数目*/   unsigned long long timestamp_last_tick; /* 最后一个调度程序的节拍*/   struct task_struct *curr; /* 当前运行任务*/   struct task_struct *idle; /* 该处理器的空任务*/   struct mm_struct *prev_mm; /* 最后运行任务的mm_struct结构体*/   struct prio_array *active; /* 活动优先级队列*/   atomic_t nr_iowait; /* 等待I/O操作的任务数目*/   …… };

    提供了一组宏来获取给定CPU的进程执行队列:  

      #define cpu_rq(cpu) //返回给定处理器可执行队列的指针   #define this_rq() //返回当前处理器的可执行队列   #define task_rq(p) //返回给定任务所在的队列指针

    在操作处理器任务队列时候要用锁:

    __task_rq_lock …… __task_rq_unlock

    4 schedule 系统要选定下一个执行的进程通过调用schedule函数完成。

    调度时机:

      l 进程状态转换的时刻:进程终止、进程睡眠;   l 当前进程的时间片用完时(current->counter=0);   l 设备驱动程序调用;   l 进程从中断、异常及系统调用返回到用户态时;

    睡眠和唤醒:

    休眠(被阻塞)的进程处于一个特殊的不可执行状态。休眠有两种进程状态:     TASK_INTERRUPTIBLE:接收到信号就被唤醒     TASK_UNINTERRUPTIBLE:忽略信号    两种状态进程位于同一个等待队列上,等待某些事件,不能够运行。

    进程休眠策略:

    //q是我们希望睡眠的等待队列 DECLARE_WAITQUEUE(wait, current); add_wait_queue(q, &wait); //condition 是我们在等待的事件 while (!condition) { //将进程状态设为不可执行休眠状态 or TASK_UNINTERRUPTIBLE set_current_state(TASK_INTERRUPTIBLE); if(signal_pending(current)) //调度进程 schedule(); } //进程被唤醒条件满足 进程可执行状态 set_current_state(TASK_RUNNING); //将进程等待队列中移除 remove_wait_queue(q, &wait);

    进程通过执行下面几个步骤将自己加入到一个等待队列中:

    1) 调用DECLARE_WAITQUEUE()创建一个等待队列的项。 2) 调用add_wait_queue()把自己加入到队列中。该队列会在进程等待的条件满足时唤醒它。当然我们必须在其他地方撰写相关代码,在事件发生时,对等待队列执行wake_up()操作。 3) 将进程的状态变更为 TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。 4) 如果状态被置为TASK_INTERRUPTIBLE,则信号唤醒进程。这就是所谓的伪唤醒(唤醒不是因为事件的发生),因此检查并处理信号。 5) 检查条件是否为真;如果是的话,就没必要休眠了。如果条件不为真,调用schedule()。 6) 当进程被唤醒的时候,它会再次检查条件是否为真。如果是,它就退出循环,如果不是,它再次调用schedule()并一直重复这步操作。 7) 当条件满足后,进程将自己设置为TASK_RUNNING并调用remove_wait_queue()把自己移出等待队列。

    二 抢占和上下文切换 进程切换schedule函数调用context_switch()函数完成以下工作:

        l 调用定义在<asm/mmu_context.h>中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中。     l 调用定义在<asm/system.h>中的switch_to(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息。在前面看到schedule函数调用有很多种情况,完全依靠用户来调用不能达到很好的效果。内核需要判断什么时候调用schedule,内核提供了一个need_resched标志来表明是否需要重新执行一次调度:     l 当某个进程耗尽它的时间片时,scheduler_tick()就会设置这个标志;     l 当一个优先级高的进程进入可执行状态的时候,try_to_wake_up()也会设置这个标志。每个进程都包含一个need_resched标志,这是因为访问进程描述符内的数值要比访问一个全局变量快(因为current宏速度很快并且描述符通常都在高速缓存中)。

    1 用户抢占 内核即将返回用户空间时候,如果need_resched标志被设置,会导致schedule函数被调用,此时发生用户抢占。 用户抢占在以下情况时产生:

        l 从系统调返回用户空间。     l 从中断处理程序返回用户空间。

    2 内核抢占 只要重新调度是安全的,那么内核就可以在任何时间抢占正在执行的任务。 什么时候重新调度才是安全的呢?只要没有持有锁,内核就可以进行抢占。锁是非抢占区域的标志。由于内核是支持SMP的,所以,如果没有持有锁,那么正在执行的代码就是可重新导入的,也就是可以抢占的。 为了支持内核抢占所作的第一处变动就是为每个进程的thread_info引入了preempt_count计数器。该计数器初始值为0,每当使用锁的时候数值加1,释放锁的时候数值减1。当数值为0的时候,内核就可执行抢占。从中断返回内核空间的时候,内核会检查need_resched和preempt_count的值。如果need_resched被设置,并且preempt_count为0的话,这说明有一个更为重要的任务需要执行并且可以安全地抢占,此时,调度程序就会被调用。

    内核抢占会发生在:

      l 当从中断处理程序正在执行,且返回内核空间之前。   l 当内核代码再一次具有可抢占性的时候。   l 如果内核中的任务显式的调用schedule()。   l 如果内核中的任务阻塞(这同样也会导致调用schedule())。

    一 用户空间和内核空间

    Linux内核将这4G字节虚拟地址空间的空间分为两部分:

      l 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”。

      l 将较低的3G字节(从虚拟地址 0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间)。

      因为每个进程可以通过系统调用进入内核,因此Linux内核由系统内的所有进程共享。于是从具体进程的角度来看,

    每个进程可以拥有4G字节的虚拟空间。如此划分提供对系统内核安全保护机制。

    系统调用

    用户空间的进程和内核空间程序如何进行交互?——系统调用

        l 为用户空间提供统一的抽象接口;     l 保证系统的安全访问和稳定;     l 控制进程用户空间与内核空间的切换;

    1 系统调用的层次关系 Linux内部体系结构:            

    系统调用过程如下:

        

    Unix系统设计理念:提供机制而不是策略 将编程问题分成两个部分:机制(Mechanism)和策略(Policy)。对外应用程序提供接口(系统调用API),而不用去关心如何实现——机制;真正的实现在系统内部,系统提供实现接口算法而不关心如何使用——策略。

    2 系统调用程序执行 通知内核的机制是靠软中断实现的: 通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。 通过异常陷入到内核中,如何执行相应的系统调用: 在x86上, 系统调用号是通过eax寄存器传递给内核的。在陷入内核之前,用户空间就把相应系统调用所对应的号放入eax中了。这样系统调用处理程序一旦运行,就可以从eax中得到数据。

     call *sys_call_table(,
    转载请注明原文地址: https://ju.6miu.com/read-678783.html

    最新回复(0)