linux用户进程分析

       经过实验3的介绍。我们须要来点实在的。所以将我们理解的流程用于linux系统的分析换句话说。通过类比的方式去进行描写叙述与理解linux相关的部分。

本节的内容非常详实。并且也分析了非常久,所以长话短说。静静的去感受与理解linux内核代码的实现。

当然。我们实验的系统代码非常easy并且直接。可是linux内核经过20多年的发展,更有成千上万的开发人员共同维护,所以对于代码的书写会更加精练,对于基础相对薄弱的程序猿去理解有一些障碍,可是反过来说。更有利于我们语言知识的提高。

    例如以下的介绍方法是相应于实验中的3个部分进行分别说明,而对于这3个部分主要以分析代码的结构与流程来介绍。而对于细节的实现还须要去认真查看代码。本文仅仅对核心的部分进行介绍,引导读者从主要的部分去学习与理解linux内核的部分代码。

一)进程管理:

    在浩如烟海的代码中,怎样寻找,太麻烦了。所以我们须要使用其它人的分析结果參考了《深入理解Linux内核第3版》的部分章节,然后依据相关介绍有针对性的查看代码。可是我们分析的对象是kernel4.0.2.跟原文中有些出入。

当然对于去理解linux内核的程序猿,当然这本书是不可多得的參考资料。可是实际使用的linux内核却是发展的。所以还是须要我们触类旁通的理解之。

    依据实验3的内容我们首先须要找到对进程的抽象描写叙述。即核心数据结构。由于其结构非常是复杂。就不在博客中列出,而是放在附件的源代码中。内核的核心数据结构task_struct——include/linux/sched.h(1286)。当我们打开该数据结构时,第1感觉肯定是定义太复杂了,从什么地方入手呢?我们能够从两个方面入手去理解:其一为从经典的操作系统教程中找寻进程的描写叙述,然后以之为切入点相应分析(能够參考的教程为《操作系统精髓与设计原理第6版》)。其二为从简单的操作系统模型去把握核心的部分去分析,然后以此为基础去逐步扩展着分析。当然。我们介绍的是第2种方法,由于我们已经知道了一个简单的操作系统模型。

    依据实验3的进程属性分析我们能够将进程属性分为例如以下几部分:

  1. 进程标识符(pid,stack

    内核有两种方式识别进程:其一为unix标准的id号(见结构体元素:pid_t pid);其二通过进程的描写叙述符地址,每一个进程都会分配唯一的一个进程描写叙述符。

    对于进程id号,须要被循环使用。它被结构体pid_namespace(pid_namespace.h)中的pidmap所指向的page进行管理。用该页帧的每一个bit位来表示已经使用的pid号。

    对于进程描写叙述符地址,由于进程描写叙述符与每一个进程是一一相应的,当内核须要对进程进行操作时,首先须要得到进程描写叙述符地址,然后对其数据进行操作;对于此linux内核用了一种 巧妙的方式来进行操作,这样的方式是基于每一个进程在内核状态都有自己唯一的堆栈。将进程运行的必要状态(线程相关状态)放在栈顶与堆栈共享内存空间详情见下图:

     由上图能够知道,内核能够通过简单的检查堆栈指针esp获得进程描写叙述符地址。

这种结构能够在sched.h中获得,当然初始化我们能够參考init_taskinit_task.c)的默认初始化。


   2.进程状态(state,exit_state)

    进程切换如上图所看到的:我们能够简单的理解。linux进程主要分为4种状态——执行,堵塞(分两种),退出(分两种),特殊(分两种)。迁移路线见图中标识,当中虚线为推測的退出路线。如上仅仅是一个大概的描写叙述图,更细节的须要针对不同的事件去查看进程状态。


    3.进程关系——层次关系(real_parent,parent,children,sibling)。静态链接关系(tasks)。执行链接关系,协作关系(进程组与会话)

    (A)层次关系:进程总是被其它进程所创建(当然01进程默认被内核创建,考虑除外),所以全部进程的创建过程决定了其层次关系(父子)例如以下图(P0创建P1,P2,P3;P3创建P4):

    (B)静态链接关系:全部进程都链接到由内存编译时创建的以init_task.task为表头的链表中。

     (C)协作关系:当进程在一起协同工作时。可能须要被组织成组。进而形成会话(这样的机制,可以參考《UNIX高级编程》的第9章)。为了描写叙述如上关系。须要将进程的pid进行分类——pidgpidsid。而在用户空间进程一般操作都是通过pid来进行处理的。所以我们须要可以从pidtask_struct地址的转换。具体描写叙述见下图表示:实现代码详情见pid.h,pid_namespace.h等代码实现。

     (D)执行链接关系:主要用于进程被调度时,被分配到不同的队列中。眼下主要描写叙述静态的结构,而动态的关系在下一章描写叙述。

    4.处理器状态——thread(线程)task(堆栈)

    线程是进程的运行单元。而运行的状态主要用堆栈保存。

进程的堆栈如前介绍,而进程的线程主要包括了处理器的寄存器状态。

    5.内存映像信息——mm_struct

    每一个用户进程都须要被载入到内存中才干执行,而每一个用户进程都是载入磁盘中的可执行文件(elf文件为例),所以从这个意义上来说,内存映像就是将elf文件载入到内存之后,内存的执行状态。为此我们须要首先了解elf文件的构成,然后依据此去理解mm_struct的相关域。同一时候,我们也须要參考实验3中进程对进程内存的引用。

    (A)elf文件具体结构能够用例如以下图描写叙述:能够通过readelf去读取其基本信息,同一时候也能够用hexdump去读取每一个部分的具体内容。

    又上图能够看到出elf文件是有不同的段组成。段的基本信息由elf文件头描写叙述(readelf -h),elf文件被载入到内存的情况由程序头(readelf -l)描写叙述,然后就是每一个elf段的具体信息(能够用hexdump -s xxx -n yyy来查看,而须要被载入的内存的段组合成了程序头中的段)。最后是elf段的信息表(readelf -S)。通过它能够查看每一个elf段的具体信息。

    当了解了elf文件之后我们还要知道进程对执行文件的内存描写叙述:代码段,数据段,堆栈段等信息,当然还有程序执行时的命令行与环境变量等信息(这些信息被放在堆栈段的栈底)。这些信息保存在mm_struct例如以下域中:

    unsigned long start_code, end_code, start_data, end_data;//代码段与数据段

    unsigned long start_brk, brk, start_stack;//堆栈信息

    unsigned long arg_start, arg_end, env_start, env_end;//命令行參数与环境变量

    mm_struct也包括它全部映射的elf文件的一些信息:

    unsigned long saved_auxv[AT_VECTOR_SIZE]; //elf表,包括了一些elf文件的信息(binfmt_elf.c:create_elf_tables()中查看其具体内容)

    struct linux_binfmt *binfmt;//载入elf文件时使用的接口

    struct file *exe_file;//相应的elf文件

    (B)进程的内存分配,通过实验3能够知道每一个进程都必需有自己的页文件夹与页表,所以有例如以下的例如以下域:pgd_t * pgd;


    (C)线性区管理:如前面所诉,全部elf文件都会被以程序段的信息载入到内存中,当载入到内存中时,分配的虚拟地址都是连续的(能够用cat /proc/$pid/maps查看)。这些线性区是是由结构体vm_area_struct所描写叙述。对于一个进程可能有非常多的线性区。并且对它的操作也非常频繁,所以须要有高速查找相关线性区的方式——内核使用了红黑树的方式。

    struct rb_root mm_rb;//线性区的红黑树根。

    相应的vm_area_struct的相关属性例如以下:

    unsigned long vm_start;//线性区的開始地址

    unsigned long vm_end;//结束地址
    pgprot_t vm_page_prot;//訪问权限

    unsigned long vm_flags;//标志

    struct file * vm_file;//指向的文件

    struct rb_node vm_rb;//线性区的红黑树节点

    struct mm_struct *vm_mm;//线性区所属的mm_struct.

    全部线性区都被链接到mm_struct上:

    struct vm_area_struct *mmap;/* list of VMAs */

    (D)初始化与设置进程mm的流程例如以下,能够參考一下,更easy理解mm_structmm_area_struct的结构体。


     6.统计信息——暂无

     7.调度管理——暂无

     二)中断管理glibc

  1. 中断基本处理

    对于中断的基本处理,我们也用类似实验3的流程图開始介绍之:

    如上图所看到的,中断的基本处理流程跟实验3的没有多少差别。仅仅是调用方式的差别,当然,我们也能够说是对x86处理器中断处理的封装不一样。

  2.系统调用:

   对于系统调用。能够參考《深入理解linux内核》第10章——系统调用的部分。例如以下仅仅是引用另外一张图来说明一下:

 

    如上图所看到的,给出我们使用系统调用的框图,而直接面向系统调用编程的软件是libc标准库,比方:glibc。bionic等。当然我们也能够直接调用系统调用。对于由系统调用进入system_call的方式由中断基本处理流程介绍了。全部的系统调用都被定义在sys_call_table的数组中。

    三)用户启动代码:

    我们所认识的C程序都是从main函数開始运行的。可是从实验3,我们发如今运行main函数前须要运行一些引导代码,为运行main函数做一些必要的铺垫,所以我们就对基于glibc的代码在linux平台上的启动做一些介绍。

为什么须要理解这些细节呢。不不过为了完好课程,更重要的是为了我们去实现一个操作系统接口,不不过标准c接口,更重要是怎样为操作系统的系统调用进行封装,并且方便其它用户使用。glibc实现这种功能,而是一个非常好的样例值得我们学习。当我们在学习软件技术时,非常好的模仿是一个入门技巧。更要感谢这么技术的一个本质属性——可以被极简单的复制。然后被不断地演化。改进。用于满足实际需求。

   以手动链接“hello world”为例,解析程序默认链接的实现。以及glibc的引导代码的简单流程:

  1. 编辑“void main(){printf(hello world. );}”到文件hello.c

  2. 编译hello.chello.ogcc -c hello.c

  3. 链接hello.ohello.out

   

    直接链接:ld -hello.o -o hello.out出现一个错误(不能找到puts)一个警告(不能找到_start)。出现错误的原因是引用了printf。它是基于标准c实现的。所以须要引用c(-lc);出现警告是由于全部程序的链接过程都是由链接脚本控制。而默认的链接动作是以_start为入口——这就是全部用户进程的实际入口(readelf -h能够查看之),它的实如今glibc编译时生成的crt1.o中定义。将libccrt1.o链接时又碰到了其它标识没有被定义,所以我们将链接的默认參数dump出来:

 /usr/lib/gcc/x86_64-redhat-linux/5.3.1/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/5.3.1/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/5.3.1/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/5.3.1 -L/usr/lib/gcc/x86_64-redhat-linux/5.3.1/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/5.3.1/../../..  -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-redhat-linux/5.3.1/crtend.o /usr/lib/gcc/x86_64-redhat-linux/5.3.1/../../../../lib64/crtn.o

    由此我们知道每一个c程序被运行前与结束须要链接的代码为crt1.o,crti.o,crtbegin.o,crtend.o,crtn.o

对于每个它们的引用与使用情况,我们能够用readelf -s读取它们定义的标识符了解一下,当然也须要将每个单独链接之后。然后执行,针对出错,进行重复尝试。

    当链接完毕之后我们执行我们的代码,又报错了,程序解析器不正确:

 ./hello.o.out: /lib/ld64.so.1: bad ELF interpreter: 没有那个文件或文件夹

默认链接器会使用如上的链接器,而实际机器使用的为glibc编译出来的/lib64/ld-linux-x86-64.so.2,所以我们使用命令——--dynamic-linker /lib64/ld-linux-x86-64.so.2解决。当我们在linux内核中分析程序载入(exec系统调用)的过程中时会发现内核不只会建立应用程序的虚拟段,同一时候也会建立解析器的虚拟段。同一时候会创建进程的运行环境——命令行与环境变量,拷贝它们到进程的堆栈空间的顶端(我们能够通过dump进程的堆栈来查看——附件process_stack.c能够參考)。

    一叶说,最终又開始接着写了,可是发现一个问题。linux的进程描写叙述与管理太复杂了。而我写的大部分内容也是參考了《深入理解linux内核》的部分内容,同一时候针对linux 4.0源代码进行分析与理解。所以此文仅仅起了一个抛砖引玉的作用。从原始模型类比的方式去理解linux内核,同一时候也能够了解linuxunix的基础上演变了非常多。当然假设须要对内核有深入的理解,參考《深入理解linux内核》是一种非常好的途径。可是也要认识到linux内核是一个不断演化,改进的过程。所以如今的理解仅仅是针对眼下的状态有效,兴许的一些理解应该跟着最新的代码走。当我们在看linux代码时也会发现非常多有效的技术在内核中非常快得到使用,比方slab分配管理,RCU机制等。然后经过几个版本号的更新就稳定下来了,然后被重复的使用。所以对于我们理解内核的代码。须要对这些技术有深入的理解。

我们能够将其作为基本模块来理解,首先理解这些技术的原理——能够通过查看这些技术的论文,同一时候參考最先引入的内核版本号进行理解,然后在跟进代码的演化实现到稳定的代码实现。最后看最新的代码实现。兴许有机会再好好分析与整理linux内核的部分实现。眼下是想把这个简单内核的课程完毕。而对操作系统的模型内核有一个完整的认识。



原文地址:https://www.cnblogs.com/yangykaifa/p/7145838.html