• 20135202闫佳歆--week 9 期中总结


    期中总结

    前半学期的主要学习内容是学习mooc课程《Linux内核分析》以及课本《Linux内核设计与实现》。
    所涉及知识点总结如下:

    1. Linux内核启动的过程——以MenuOS为例


    1.1 计算机的启动过程

    1. CPU启动后,BIOS程序开始执行,检测硬件,然后加载引导程序BootLoader和硬盘的第一个扇区MBR。
    2. BootLoader会将操作系统初始化,启动操作系统。

    Linux内核的启动有三个参数:

    • kernel
    • initrd
    • root所在目录、分区。

    内核会首先生成0号进程idle,然后0号进程产生1号进程init,1号进程是所有用户态进程的祖先,0号进程是所有内核线程的祖先。
    1号进程是Linux启动后所执行的第一个进程;
    0号进程是当没有进程可以执行时,会执行0号进程。

    1.2 进程的描述

    进程的描述依靠进程描述符task_struct数据结构,部分定义如下:

    struct task_struct {
        volatile long state;	/* 进程状态 */
        void *stack;    /* 进程的内核堆栈 */
        atomic_t usage;    	    
        unsigned int flags;	/* 每个进程的标识符 */
        unsigned int ptrace;
            
        / #ifdef CONFIG_SMP   // 条件编译,SMP多处理器相关
        
        ……
        int on_rq   // 运行队列相关,下面几行是进程队列和调度相关。
        ……
            
        struct list_head tasks  // 进程链表
        
            ……
            
        next_task
        prev_task   // 对进程链表的管理
            
        tty_struct  // 控制台
            
        fs_struct   // 文件系统
        struct files_struct *files; // 打开的文件描述符列表
            
        file_struct // 打开的文件描述符
            
        mm_struct   // 内存管理描述
        struct mm_struct *mm, *active_mm;   // 地址空间,内存管理。
            
        signal_struct   // 进程间通信、信号描述
            
        struct list_head ptraced    // 调试用
            
        utime
        stime   // 进程时间相关
        
    }
    

    进程的唯一标示是pid。

    1.3 进程的创建

    创建进程,其本质是调用fork函数创建一个子进程,从1号进程开始,都是使用的这种方法。

    Linux通过clone()系统调用实现fork()。
    创建进程的大概步骤如下:

    1. fork()、vfork()、__clone()都根据各自需要的参数标志调用clone()。
    2. 由clone()去调用do_fork()。
    3. do_fork()调用copy_process()函数,然后让进程开始运行。
    4. 返回do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。

    部分代码示意如下:

    /* 复制一个PCB */
    err = arch_dup_task_struct(tsk, orig);
    
    /* 要给新进程分配一个新的内核堆栈 */
    ti = alloc_thread_info_node(tsk, node);
    tsk->stack = ti;
    setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
    
    /* 要修改复制过来的进程数据,比如pid、进程链表等 */
    *childregs = *current_pt_regs(); //复制内核堆栈
    childregs->ax = 0; //eax设置为0,所以子进程返回值为0.
     
    p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
    p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
    
    
    dup_task_struct // 复制pcb
    alloc_thread_info_node  // 创建了一个页面,其实就是实际分配内核堆栈空间的效果。
    setup_thread_stack  // 把thread_info的东西复制过来
    

    也就是说,子进程的创建绝大部分内容是直接复制了父进程的进程描述符,这是它们有共同的地址空间,堆栈,和进程上下文,当写时复制的时候才会区分开。
    最开始学习fork的时候提到,fork是“一次调用,两次返回”,父进程返回子进程的pid,子进程返回0,这是因为子进程复制父进程相关数据的时候,将eax设置为0。
    除此之外,还有特殊设置的是sp和ip,sp指向调度到子进程时的内核栈顶,ip指向ret_from_fork,子进程就是从这条指令开始执行的。

    1.4 进程调度

    Linux采用的是CFS调度算法,有四个组成部分:

    CFS调度算法

    Linux的调度基于分时优先级

    • Linux的进程根据优先级排队
      • 根据特定的算法计算出进程的优先级,用一个值表示
      • 这个值表示把进程如何适当的分配给CPU
    • Linux进程中的优先级是动态的
      • 调度程序会根据进程的行为周期性地调整进程的优先级
      • 例如:
        • 较长时间为被分配到cpu——↑
        • 已经在cpu上运行了较长时间——↓

    常见的一些函数:

    nice 
    getpriority/setpriority 设置优先级
    sched_getschedduler/sched_setscheduler
    sched_getparam/sched_setparam
    sched_yield
    sched_get_priority_min/sched_get_priority_max
    sched_rr_get_interval
    

    进程调度的时机

    • 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();主动调度。
    • 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。用户态进程只能被动调度。
    • 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程既可以主动调度,也可以被动调度

      内核线程是只有内核态没有用户态的特殊进程

    1.5 抢占和上下文切换

    enter description here

    need_resched标志:
    内核用这个标志来表明是否需要重新执行一次调度。

    1. 当某个进程应该被抢占时,scheduler_tick()会设置这个标志。
    2. 当一个优先级高的进程进入可执行状态时,try_to_wake_up()会设置这个标志。
    3. 内核检查这个标志确认其被设置,调用schedule()来切换到一个新的进程。
    4. 该标志对于内核来说是一个信息,表示youqitajinc应当被运行了,要尽快调用调度程序。
    5. 再返回用户空间以及从中断返回时,内核也会检查标志。
    6. 每个进程都包含一个need_resched标志,因为访问进程描述符里的数值比访问一个全局变量要快。

    锁是非抢占区域的标志。实现如下:
    为每个进程的thread_info中加入preempt_count计数器,初值为0,使用锁+1,释放锁-1,数值为0时,可以执行抢占。

    enter description here

    • 挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行,但是是同一个进程,而进程上下文的切换是两个进程在切换。
    • 进程上下文包含了进程执行需要的所有信息
      • 用户地址空间:包括程序代码,数据,用户堆栈等
      • 控制信息:进程描述符,内核堆栈等
      • 硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
    • schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换

    2.可执行程序的装载


    2.1 预处理、编译、链接和目标文件的格式

    enter description here

    enter description here

    查看一个可执行文件头部内容:

    readelf -h
    

    头部后是代码和数据,等等。

    可执行程序加载的主要工作:
    当创建或者增加一个进程映像的时候,系统在理论上将拷贝一个文件的段到一个虚拟的内存段

    静态链接的ELF可执行文件和进程的地址空间
    32位x86进程地址空间共4G,1G是内核空间。
    如何加载到内存?
    默认从0x8048000开始加载,然后头部需要占用一定空间,程序的实际入口可以在0x8048100等地方,即可执行文件加载到内存中开始执行的第一行代码的入口处。

    一般静态链接会把所有代码放在一个代码段,
    动态链接会有多个代码段。

    2.2 可执行程序的装载

    enter description here

    2.2.1 sys_execve的内部处理过程

    1604    SYSCALL_DEFINE3(execve,
    1605    		const char __user *, filename,
    1606    		const char __user *const __user *, argv,
    1607    		const char __user *const __user *, envp)
    1608    {
    1609    	return do_execve(getname(filename), argv, envp);
    1610    }
    1611    #ifdef CONFIG_COMPAT
    1612    COMPAT_SYSCALL_DEFINE3(execve, const char __user *, filename,
    1613    	const compat_uptr_t __user *, argv,
    1614    	const compat_uptr_t __user *, envp)
    1615    {
    1616    	return compat_do_execve(getname(filename), argv, envp);
    1617    }
    1618    #endif
    

    sys_execve函数中返回了一个do_execve:

    1549    int do_execve(struct filename *filename,
    1550    	const char __user *const __user *__argv,
    1551    	const char __user *const __user *__envp)
    1552    {
    1553    	struct user_arg_ptr argv = { .ptr.native = __argv };
    1554    	struct user_arg_ptr envp = { .ptr.native = __envp };
    1555    	return do_execve_common(filename, argv, envp);
    1556    }
    
    最后一句中do_execve_common把文件名,参数和环境转换了一下。
    

    该函数do_execve_common打开如下:

    1474    	file = do_open_exec(filename);
    打开了一个要加载的可执行文件,然后会加载一下它的头部,建立一个结构体,把命令行参数和环境变量拷贝到结构体中;
    
    1513    	retval = exec_binprm(bprm);
    对这个可执行文件的处理过程。
    

    打开exec_binprm这个函数,可以找到一句重要代码:

    1416    	ret = search_binary_handler(bprm);
    寻找这个我们打开的可执行文件的处理函数。
    

    打开search_binary_handler,找到list_for_each_entry如下:

    1369	list_for_each_entry(fmt, &formats, lh) {
    1370		if (!try_module_get(fmt->module))
    1371			continue;
    1372		read_unlock(&binfmt_lock);
    1373		bprm->recursion_depth++;
    1374		retval = fmt->load_binary(bprm);
    1375		read_lock(&binfmt_lock);
    1376		put_binfmt(fmt);
    1377		bprm->recursion_depth--;
    1378		if (retval < 0 && !bprm->mm) {
    1379			/* we got to flush_old_exec() and failed after it */
    1380			read_unlock(&binfmt_lock);
    1381			force_sigsegv(SIGSEGV, current);
    1382			return retval;
    1383		}
    1384		if (retval != -ENOEXEC || !bprm->file) {
    1385			read_unlock(&binfmt_lock);
    1386			return retval;
    1387		}
    1388	}
    
    在这个循环里寻找能够解析这个当前可执行文件的代码模块。
    
    retval = fmt->load_binary(bprm);    //  这一句中的load_binary,加载处理函数。这一句是函数指针,实际上是调用的load_elf_binary。
    

    2.2.2 load_elf_binary的赋值和注册

    /* 全局变量elf_format,把函数指针load_elf_binary**赋值**给了.load_binary */
    82  static struct linux_binfmt elf_format = {
    83    .module     = THIS_MODULE,
    84    .load_binary    = load_elf_binary,
    85    .load_shlib = load_elf_library,
    86    .core_dump  = elf_core_dump,
    87    .min_coredump   = ELF_EXEC_PAGESIZE,
    88  };
    
    /* 把变量elf_format**注册**进了format链表里,就可以在链表里对应elf模式中找到对应模块 */
    2198    static int __init init_elf_binfmt(void)
    2199    {
    2200        register_binfmt(&elf_format);
    2201        return 0;
    2202    }
    

    在load_elf_binary中调用了start_thread这个函数:

    198 start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)/* pt_regs 是内核堆栈栈底的函数,*/
    199 {
    200 	set_user_gs(regs, 0);
    201 	regs->fs		= 0;
    202 	regs->ds		= __USER_DS;
    203 	regs->es		= __USER_DS;
    204 	regs->ss		= __USER_DS;
    205 	regs->cs		= __USER_CS;
    206 	regs->ip		= new_ip;   //起点位置
    207 	regs->sp		= new_sp;
    208 	regs->flags		= X86_EFLAGS_IF;
    209 	/*
    210 	 * force it to the iret return path by making it look as if there was
    211 	 * some work pending.
    212 	 */
    213 	set_thread_flag(TIF_NOTIFY_RESUME);
    214 }
    215 EXPORT_SYMBOL_GPL(start_thread);
    

    2.2.3 动态链接与静态链接

    动态链接库的执行过程

    887	if (elf_interpreter) {
    888		unsigned long interp_map_addr = 0;
    889
    890		elf_entry = load_elf_interp(&loc->interp_elf_ex,
    891					    interpreter,
    892					    &interp_map_addr,
    893					    load_bias);
    需要加载连接器
    

    静态链接的执行过程

    912     else {
    913		    elf_entry = loc->elf_ex.e_entry;
    直接把elf文件的entry地址赋给elf_entry。
    

    但是在start_thread中是直接用的elf_entry:

    start_thread(regs,elf_entry, bprm->p);
    
    1.如果是一个静态连接的文件,elf_entry就是指的main函数开始的位置
    2.如果是一个需要依赖动态链接库的文件,elf_entry指向的是动态链接器的起点,将cpu控制权交给ld来加载依赖库并完成动态链。
    

    3. 系统调用


    3.1 用户态和内核态

    用户通过库函数与系统调用联系起来。

    • 内核态
      在高执行级别下,代码可以执行特权指令,访问任意的物理地址
    • 用户态:
      代码的掌控范围受到限制。

    intel x86 CPU有四个权限分级,0-3。Linux只取两种,0是内核态,3是用户态
    区分权限级别使得系统更加稳定。

    如何区分用户态与内核态?
    cs:eip。[代码段选择寄存器:偏移量寄存器]
    通过cs寄存器的最低两位,表示当前代码的特权级:
    【针对逻辑地址】
    0xc0000000以上的空间只能在内核态下访问
    0x00000000-0xbfffffff两种状态下都可以访问

    如何进行切换?
    中断。

    3.2 中断处理过程

    中断处理是从用户态进入内核态的主要方式。

    • 寄存器上下文
      从用户态切换到内核态时,必须保存用户态的寄存器上下文到内核堆栈中,同时会把当前内核态的一些信息加载,例如cs:eip指向中断处理程序入口。
    • 中断发生后的第一件事就是保存现场 - SAVE_ALL
      中断处理结束前最后一件事是恢复现场 - RESTORE_ALL

    3.3 系统调用的“三张皮”

    enter description here

    3.4 系统调用在内核代码中的处理过程

    以system_call为例:

    trap_init里面有一个set_system_trap_gate函数,函数定义中有系统调用的中断向量SYSCALL_VECTOR****和汇编代码入口system_call
    一旦执行int 0x80,系统直接跳转到system_call来执行。

    enter description here

    从以上可以看出:

    1. 在系统调用返回之前,可能发生进程调度,进程调度里就会出现进程上下文的切换
    2. 进程间通信可能有信号需要处理

    基本的知识点简略总结如上。
    总而言之,这半个学期以来,我们学习了有关Linux内核的相关知识,尽管有视频和书的双重工具,但是其实我们的学习还是比较浅显的,对于内核的理解还有很多的不足,需要在下半学期的学习和实践中不断补足。


    学习《Linux内核分析》课程中最大的收获:
    最大的收获是转变了学习方式,通过反转课堂、mooc的方式锻炼了自己的学习能力。并且,之前我对Linux并不是很熟悉,而这个课程过后我有了一个比较基本的全面的认识,这为我之后的学习提供了很多帮助。

    学习完《Linux内核分析》课程后您最大的遗憾:
    学习的内容还是比较浅显,调试的时候动作也比较简单,只能说是基本完成了学习任务,但却还有很多需要提升的地方,所以我需要在之后的学习过程中更加严格要求自己,继续学习研究Linux相关。

    附录:学习笔记链接总结


    Linux及安全课程——相关链接总结


    闫佳歆 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000

  • 相关阅读:
    性能碾压 POI !利用模板语法快速生成 Excel 报表
    大庆金桥:基于 SpreadJS 开发实现计量器具检定证书的在线生成与打印
    厦门科云:构建基于 SpreadJS 的管理会计综合实训平台
    为什么 Vue 更符合这个时代的大势所趋
    嵌入SpreadJS,赋能计量器具检定信息化
    GcExcel:比 Apache POI 速度更快、性能更高
    使用SpreadJS 开发在线问卷系统,构筑CCP(云数据采集)平台
    首厚智能:嵌入 SpreadJS 表格组件,搭建实验室信息管理系统(LIMS)
    企业数字化转型:用 SpreadJS 打造互通互链的电力系统物联网
    悲观锁和乐观锁
  • 原文地址:https://www.cnblogs.com/20135202yjx/p/5422319.html
Copyright © 2020-2023  润新知