美文网首页
Linux进程基础行为(一)

Linux进程基础行为(一)

作者: FlyingReganMian | 来源:发表于2018-03-08 11:36 被阅读0次

    1.进程组织结构

    1. task_struct ,thread_info 和内核栈

    image.png image.png

    在内核中通常current宏获取当前正在运行的task_struct。对于不同的硬件体系current的实现方式不一样,寄存器较多的体系直接用一个寄存器来存储当前进程的task_struct的指针,X86 current把内核栈栈顶指针最后13位(内核栈8KB)清零,计算出thread_info的位置,通过thread_info总task指针指向当前进程的task_truct

    2. 进程状态

    image.png

    3. 进程树

    进程树的树根是0号进程idle,是所有进程的祖先。

    1. 0号进程创建1号进程(创建时是内核线程),1号进程负责内核部分的初始化工作及系统配置,创建用于高速缓存和虚拟内存管理的内核线程
    2. 1号进程调用execve()运行磁盘上的init可执行程序,并演变为用户态1号进程,init进程
    3. init进程按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号,2号,……的Getty进程(初始化终端)
    4. getty进程监控到终端连接信号时,通过调用execve()执行login登录程序
    5. 如果登录成功过,login程序通过execve()函数调用shell
    6. Shell进程接收getty进程的pid,取代原来的getty进程。

    1. 0号进程idle

    1. idle进程只能从静态地填写thread_info和task_struct
    2. idle进程让CPU陷入空闲循环,空闲运行
    3. 多处理器上刚启动只有一个CPU能运行,只有CPU0上的idle进程完成初始化后才激活其他CPU,并通过copy_process()创建其他CPU的idle进程

    3. 进程创建与撤销

    在Linux系统中,系统通过fork()函数复制一个现有的进程创建一个新进程。
    接着,调用exec()函数创建新进程的地址空间,并把新程序载入其中。
    最终,程序通过调用exit()系统调用退出运行。
    进程退出后被设置为僵死状态,直到父进程检查之后调用wait()在删除进程描述符资源

    1. 进程创建

    1. fork ,vfork, clone

    fork: 写时复制,赋值父进程的task_struct,thread_info和内核栈,mm_struct,页表
    vfork: 没有创建父进程的数据副本,子进程与父进程共享数据。子进程创建之后,子进程立即调用execve()加载新的进程映象,或者与父进程共享进程映象。在子进程退出之前,父进程处于阻塞状态。
    clone: fork,vfork,__clone库函数都是通过不同的参数调用clone()函数,来实现进程的创建的。

    clone()-->do_fork()(1. copy_process() 2. wake_up_task() 3. 如果是vfork调用的,则使用wait_for_vfork_done() 等待子进程结束或调用execve())
    copy_process()
    (1)复制父进程的内核栈,thread_info,task_struct
    (2)检查创建该子进程后,没有超出系统的资源限制
    (3)着手将子进程和父进程区分开来,子进程一些统计信息清空
    (4)将子进程的状态标志设置为TASK_UNINTERRUPTIBLE,保证子进程不会运行
    (5)更新子进程flag,例如设置子进程没有被exec调用的标志,超级用户权限标志清零
    (6)根据传递进clone的参数,子进程拷贝或共享父进程地址空间,打开的文件,文件系统信息等
    (7)返回一个指向子进程的指针
    wake_up_task()
    将子进程添加到调度队列上等待调度获取CPU获取执行。

    创建线程:

    image.png

    线程的用户态堆栈,task_struct,内核栈和thread_info是私有的

    创建进程和写时复制:


    image.png

    2. execve()系统调用

    前面进程只是创建了一个与父进程大体一致的子进程(仅仅是资源共享上的区别)。execve使用指定可执行文件替换现有进程上的进程印象。
    举一个例子:在Shell中运行一个二进制程序。
    (1)Shell进程将参数 运行时环境添加到自己的用户态堆栈中
    (2)Shell使用fork()创建一个子进程,放到CPU调度队列上。
    (3)子进程执行execve()使用二进制程序替换原来的Shell映象并将EIP指向新程序的入口开始执行。
    execve():
    (1)execve()--》sys_execve()--》do_execve()——》do_execve_common()
    (2)do_execve_common()执行:
    (1) unshared_files()为新进程复制一份打开的文件表
    (2) kazlloc()分配一个可执行文件的结构体linux_binprm,将可执行文件运行时所要用的参数,运行时环境等信息添加到linux_binprm。
    (3)open_exec() 打开可执行文件。具体参考文件系统
    (4) 找到负载最小的CPU。
    (5)填写linux_binprm实例,创建进程映象,将参数传入新进程的用户态堆栈。
    (6)执行新程序,使用过load_elf_binary()执行处理ELF的函数。
    (7)如果该程序是静态链接的,则load_elf_binary()直接返回新程序的入口地址,并调用start_thread()将EIP的值置为新程序的入口地址。

    2.进程撤销

    进程要终结的时候会调用exit()函数,最终通过调用do_exit()这个函数来结束进程的。
    (1)简单的说,就是将各个引用计数器减一,若有计数器归0,则释放相应的内存。
    (2)然后调用exit_notify()告知父进程为子进程找养父,并把进程状态设置为EXIT_ZOMBIE.最后调用schedule()切换到其他进程。
    (3)自此该进程不会被再次调用。此时该进程占用的内存仅为内存栈,thread_info和task_struct。
    (4)当父进程调用wait或waitpid检测到僵尸进程提供的信息之后,将进程所持有的剩余内存释放。至此进程终结。wait挂起当前进程知道一个子进程退出。

    1.孤儿进程,僵尸进程

    孤儿进程: 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

    僵尸进程: 一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

    4. 进程切换

    进程切换主要包括:1.当前进程信息压入内核栈 2. 切换CPU页表寄存器 3.切入进程工作现场恢复
    进程切换是通过schedule()函数完成的。schedule中最核心的就是switch_mm()切换进程映象,内核堆栈和硬件上下文的切换switch_to()

    1. 页表切换

    switch_mm() load_cr3(next->pgd)

    image.png

    2.内核堆栈切换

    每个进程的内核空间都是一样的,因此,尽管在switch_mm()中切换了页表但是对switch_to没有影响,所以说switch_to是平滑的。

    image.png

    switch_to(pre,next,last)
    pre:当前进程 next:下一个进程 last:上一个进程
    (a)页表切换后内核空间分布状况
    (b)将当前进程的内核栈栈底和进程状态信息保存在内核栈中。
    (c)将当前进程的栈顶ESP保存在prev->thread.sp中,将ESP = next->thread.sp,此时完成了内核切换,同时current宏根据P1的ESP找到P1的thread_info,进而找到task_struct。此时,内核状态为(d)
    (e)将P0的EIP保存到pre->thread.ip 此时状态为(f)
    (g)执行__switch_to()完成一些浮点部件现场保留和恢复
    (i)EIP=next->thread.ip,继续执行P1上次schedule()被打断的指令,在通过系统调用返回返回到用户空间执行代码。其中,内核态和用户态切换是通过任务状态段TSS,来获取内核或用户态的堆栈地址的。

    5. 进程调度

    1. 基本框架

    实时任务调度算法:FIFO和RR。普通任务完全公平调度算法

    image.png

    四个调度类优先级:stop_sched_class,rt_sched_class, fair_sched_class,idle_sched_class,NUll
    运行队列:

    image.png image.png

    put_prev_task():通知当前进程准备调出
    pick_next_task()选择要调入的进程

    2. CFS

    CFS为每一个进程设置一个虚拟时间,调度时总是选择虚拟时间推进缓慢的进程先运行。优先级越高,虚拟时间推进越慢,所占用CPU时间就越高。
    虚拟时间推进:

    image.png

    内核中并不直接使用进程的优先级数据值,而是将优先级转换为权重计算虚拟时间的推进。任务的nice值每降低1,则多获得10%的CPU时间。

    挑选下一个任务:普通任务的运行队列的结构是红黑树。每次挑选红黑树最左侧的叶子节点,将叶子节点缓存起来,每次挑选时,只要从这个缓存区取出该进程就行,如果该缓存区为空,则调用idle进程。

    调度延迟:CFS为无线小调度周期设置了一个“目标延迟”默认是20ms,即保证每个可运行的进程都应该至少运行一次的某个时间间隔.。越小的周期带来越好的交互性,同时也接近完美。如果挥动进程的数目超过该上限, 则延迟周期也成比例的线性扩展.。周期长度是
    __sched_period = sysctl_sched_latency * nr_running / sched_nr_latency

    3.实时策略

    FIFO:不适用时间片,可以一直运行下去,除非有更高优先级的任务抢占
    RR:是带有时间片的FIFO,只能在同一优先级的任务中轮询。

    6 其他

    1 进程和线程区别

    1. 进程是资源分配的基本单位,线程是调度的基本单位。
    2. 实体间(进程间,线程间,进线程间)通信方式的不同
      进程:A.共享内存 B.消息队列 C.信号量 D.有名管道 E.无名管道 F.信号 G.文件 H.socket
      线程: 线程间的通信方式上述进程间的方式都可沿用,且还有自己独特的几种:A.互斥量 B.自旋锁 C.条件变量 D.读写锁 E.线程信号 G.全局变量
    3. 进程有父子关系,线程只有一个父线程,其他都为子线程

    多线程的优势:

    1. 理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
    2. 理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
    3. 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
    4. 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
    5. 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

    2 用户线程 内核线程

    两种类型的内核线程:
    线程按周期性间隔运行,检测特定资源的使用,在用量超出或者低于预置的限制时采取行动
    在线程启动后则一直等待,直到内核线程请求执行某一特定的操作。
    不同之处在于:
    内核线程只工作在内核态中;而用户线程则既可以运行在内核态(执行系统调用时),也可以运行在用户态;
    内核线程没有用户空间,所以对于一个内核线程来说,它的0 ~ 3G的内存空间是空白的,它的current->mm是空的,与内核使用同一张页表;而用户线程则可以看到完整的0~4G内存空间。

    3.进程状态

    image.png

    相关文章

      网友评论

          本文标题:Linux进程基础行为(一)

          本文链接:https://www.haomeiwen.com/subject/pvnefftx.html