zoukankan      html  css  js  c++  java
  • kernel笔记——内核编译与进程管理

    内核与操作系统

    由于一些商业操作系统设计上的缺陷以及日益庞杂,“操作系统”的概念对很多人而言变得含糊不清。在进一步讨论Linux内核的话题前,我们先区分“内核”与“操作系统”这两个概念。

     

    •  操作系统:指在整个系统中完成最基本功能和系统管理的部分,包括内核、设备驱动、文件管理工具、系统管理工具、shell命令行或其他用户界面(gnome/KDE等)
    • 内核:是操作系统的核心,完成进程管理、cpu调度、内存管理、中断处理等功能

     

    一般我们编写的应用程序,跑在操作系统上,完成文字编辑、音乐播放、网页游览等特定功能。

     

    内核编译

    内核源码一般放在/usr/src目录下,我们也可以从这里获取所需内核版本的源码包。编译内核的第一步是配置内核功能,例如配置是否支持对称多处理器(SMP),可通过设置CONFIG_SMP的值。

     

    通常我们使用"make menuconfig"命令进行配置,其提供了友好的配置界面:

     
     

    保存配置后,源码目录下将生成.config配置文件,打开该文件,可以看到其内容为各种选项设置:

     

    1. CONFIG_X86_64=y
    2. CONFIG_64BIT=y
    3. CONFIG_X86=y
    4. CONFIG_SEMAPHORE_SLEEPERS=y
    5. CONFIG_MMU=y
    6. ……

    我们也可以使用当前的内核配置,使用以下命令快速地生成.config文件:

     

    1. zcat /proc/config.gz > .config 

    之后根据.config配置,对源码进行编译:

    1. make -j4

    以上使用-j选项,指定并行编译工作任务数目,在多核环境下,减少了编译时间。

     

    编译完成后生成内核压缩镜像:

     

    1. make bzImage

    生成的内核压缩镜像文件位于 arch/x86/boot目录下:

     

    1. linux-2.6.32.59 # ll arch/x86/boot/bzImage
    2. -rw-r--r-- 1 root root 2814112 07-02 22:27 arch/x86/boot/bzImage

    接着安装内核模块:

     

    1. make modules_install

    新的模块会被放置在/lib/modules目录下:

     

    1. /lib/modules # ll
    2. 总计 8
    3. drwxr-xr-x 4 root root 4096 03-08 23:53 2.6.32.12-0.7-default
    4. drwxr-xr-x 3 root root 4096 07-02 23:31 2.6.32.59-0.7-default

    最后执行make install安装内核,在/boot目录下将生成System.map、vmlinuz和initrd文件:

     

    1. linux-2.6.32.59 # make install
    2. sh /home/lx/kernel/linux-2.6.32.59/arch/x86/boot/install.sh 2.6.32.59-0.7-default arch/x86/boot/bzImage
    3. System.map "/boot"
    4. Kernel image: /boot/vmlinuz-2.6.32.59-0.7-default
    5. Initrd image: /boot/initrd-2.6.32.59-0.7-default
    6. ……

     完成安装后,在/boot/grub/menu.lst文件中增加了新内核相应的启动项,我们可以修改该文件,指定系统启动后使用新编译的内核。

     

     

    进程与线程

    Linux下,进程与线程的最大不同是进程拥有独立的内存地址空间,而线程与其他线程共享内存地址空间。除此之外,进程与线程的实现基本相同,都有task_struct结构,都被分配PID。

    内核线程没有独立的地址空间,它们完成特定工作并接受内核的调度,不同于一般用户进程,它们不接收kill命令发送的信号:

     

    1. F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME   CMD
    2. 1 S root  2   1  0 -40  -   -  0 migrat Jul01 ? 00:00:00 [migration/0]
    3. 1 S root  3   1  0  94 19   -  0 ksofti Jul01 ? 00:00:00 [ksoftirqd/0]
    4. 5 S root 18   1  0  70 -5   -  0 worker Jul01 ? 00:00:00 [events/0]
    5. ……

     

    task_struct

    task_struct结构包含进程使用的虚拟内存、打开的文件、进程状态、进程pid等信息,占用的内存由slab分配,在文件中定义。thread_info结构的第一个字段为task_struct类型的指针,当进程创建时,thread_info存放在进程内核栈的顶部:

     

     current全局变量指向当前运行进程的task_struct结构,由于thread_info存放的位置固定,这样我们通过以下汇编指令就能很容易地计算出current的值:

    1. movl $-8192, %eax
    2. andl $esp, %eax

     

    进程状态

    进程可处于以下几种状态:

     

    1. RUNNING 
    2. INTERRUPTABLE 
    3. UNINTERRUPTABLE
    4. STOP    
    5. ZOMBIE

    这些进程状态作为宏,在sched.h文件中被定义。

     

    • RUNNING状态表示进程是可执行的,或正在执行或在运行队列中,这些进程占用或等待cpu资源。
    • 进程调用退出函数exit后,进程中止,进入ZOMBIE状态。在ZOMBIE状态下进程不会占用cpu,但因其task_struct结构尚未释放,仍占用一点内存,直到父进程调用wait函数接受子进程遗愿,假如父进程先于子进程退出,则由init进程接受子进程遗愿。如果一个进程长期处于ZOMBIE状态,则是父进程中未调用wait,为程序编码问题。
    • UNINTERRUPTABLE状态表示进程不可中断,处于此状态的进程处于内核态,并且不接收任何信号。

     

     

    设置进程状态的函数为set_task_state函数,在文件中定义。

     

    进程间关系

    进程间关系与目录结构一样,为树状结构,目录结构以/为根,而进程关系以init为根。我们可以使用pstree查看进程间关系:

     

    1. linux-14:~ # echo $$
    2. 10939
    3. linux-14:~ # pstree -G -p 10939
    4. bash(10939)─┬─pstree(12806)
    5.                    └─sh(12796)───sleep(12801)

    内核代码中提供了一条双向闭环链表,自init进程始,链表连接了所有进程的task_struct结构,可以通过for_each_process宏遍历系统的所有进程:

     

    1. #define for_each_process(p)
    2.          for (p = &init_task ; (p = next_task(p)) != &init_task ; )

     

    进程创建

    Linux kernel将进程创建的步骤分成两步:fork和exec。fork生成子进程的pid,将父进程执行上下文、打开的文件描述符等内容复制一份给子进程;exec将子进程自己的执行上下文加载进内存地址空间。有以下fork例子,问执行该程序将输出多少个1?

     

    1. #include <stdio.h>
    2. #include <unistd.h>
    3. int main()
    4. {
    5.         int i;
    6.         for(i=0 ; i < 10; i++)
    7.         {
    8.                 fork();
    9.         }
    10.         printf("%d ", 1);
    11.         return 0;
    12. }

    fork拷贝父进程的内容到子进程,开销较大,假若调用fork之后马上调用exec,子进程加载自己的执行文件,则拷贝的动作就是多余的。写时拷贝(copy-on-write,COW)解决了拷贝带来无谓开销的问题,在子进程写父进程地址空间时,才触发拷贝的动作。不做多余事情、非到不得已的时候才完成工作,这也是Linux kernel高效的原因之一。

     

    内核中do_fork函数完成fork调用的工作,do_fork调用copy_process。copy_process函数中主要完成以下工作:

     

    1. 调用dup_task_struct函数申请新进程的task_struct、thread_info结构
    2. 根据clone_flags标志,调用copy_files、copy_fs、copy_mm等函数完成文件、文件系统、内存等信息的拷贝
    3. 调用alloc_pid申请新进程pid

     

    fork返回两次,在do_fork函数中实现。

     

    进程中止

    最终进程会调用exit函数中止,exit系统调用最终会调用内核中do_exit函数,do_exit在中定义,其完成以下工作:

     

    1. 调用exit_signals设置进程flags标志为PF_EXITING
    2. 调用exit_mm、exit_files、exit_fs等函数释放进程内存、文件、文件系统等结构
    3. 设置进程的exit_code
    4. 调用exit_notify,向当前进程的父进程、子进程发送信号,告知当前进程将要中止,并设置当前进程退出状态exit_state为EXIT_DEAD或EXIT_ZOMBIE
    5. 调用schedule,切换到另一个进程,从do_exit函数不会返回到调用它的函数

     

     

    Reference: Chapter 1 to chapter  3,  Linux kernel development.3rd.Edition

     

  • 相关阅读:
    三元表达式 列表和字典推导式 函数对象 名称空间 作用域 global和nonlocal 函数装饰器 枚举对象
    函数参数 打散机制 字符串比较 返回值
    函数简介
    三种字符串的介绍 文件的读写
    字符编码
    数据类型及其常用方法 数据类型转换 可变与不可变 值拷贝与深浅拷贝
    流程控制 while和for循环
    变量命名规范 常量 输入和输出 注释 数据类型 运算符 逻辑运算符
    语言分类 编译型和解释型语言分析 环境变量 代码执行的方式 pip介绍 变量
    Python django tests
  • 原文地址:https://www.cnblogs.com/felixzh/p/9047732.html
Copyright © 2011-2022 走看看