第一次作业:基于Linux进程模型分析

一、关于线程和进程

1、进程 

进程是指在系统中正在运行的一个应用程序

2、线程

线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元

3、进程与线程的关系 

· 对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程

· 一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓基于多线程的多任务

· 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

· 相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。 

4、进程与线程的区别

·进程和线程的主要差别在于它们是不同的操作系统资源管理方式

·从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。

5、如何形象地理解线程与进程

以沙箱为例进行阐述。一个进程就好比一个沙箱。线程就如同沙箱中的孩子们。

孩子们在沙箱子中跑来跑去,并且可能将沙子攘到别的孩子眼中,他们会互相踢打或撕咬。

但是,这些沙箱略有不同之处就在于每个沙箱完全由墙壁和顶棚封闭起来,无论箱中的孩子如何狠命地攘沙,他们也不会影响到其它沙箱中的其他孩子。

因此,每个进程就象一个被保护起来的沙箱。未经许可,无人可以进出。

 

二、LINUX 开源操作系统

1、关于LINUX

·Linux是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。

·它能运行主要的UNIX工具软件、应用程序和网络协议。

·它支持32位和64位硬件

·它是一个性能稳定的多用户网络操作系统。它主要用于基于Intel x86系列CPU的计算机上。

2、其他

具体的文件系统,文件结构,启动流程,加载程序,硬盘分区,桌面环境与使用技巧请参考

https://baike.so.com/doc/5349227-5584683.html

 

三、LINUX操作系统如何组织进程

1、进程分类

·系统进程:可以执行内存资源分配和进程切换等管理工作;而且,该进程的运行不受用户的干预,即使是root用户也不能干预系统进程的运行。
· 用户进程:通过执行用户程序、应用程序或内核之外的系统程序而产生的进程,此类进程可以在用户的控制下运行或关闭。
针对用户进程,又可以分为交互进程、批处理进程和守护进程三类。
· 交互进程:由一个shell终端启动的进程,在执行过程中,需要与用户进行交互操作,可以运行于前台,也可以运行在后台。
· 批处理进程:该进程是一个进程集合,负责按顺序启动其他的进程。
·守护进程:守护进程是一直运行的一种进程,经常在linux系统启动时启动,在系统关闭时终止。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。例如httpd进程,一直处于运行状态,等待用户的访问。还有经常用的crond进程,这个进程类似与windows的计划任务,可以周期性的执行用户设定的某些任务。

2、进程初始化

 

在boot/目录中引导程序把内核从磁盘上加载到内存中,并让系统进入保护模式下运行后,就开始执行系统初始化程序init/main.c。该程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理,中断处理,块设备和字符设备,进程管理以及硬盘和软盘硬件进行初始化处理。在完成了这些操作之后,系统各部分已经处于可运行状态。此后程序把自己"手工"移动到任务0(进程0)中运行,并使用fork()调用首次创建出进程1。在进程1种程序将继续进行应用环境的初始化并执行shell登陆程序。而原进程0则会在系统空闲时被调度执行,此时任务0仅执行pause()系统调用,并又会调用调度函数。

 

  "移动到任务0种执行"这个过程由宏move_to_user_mode(include/asm/system.h)完成。它把main.c程序执行流从内核态(特权级0)移动到了用户态(特权级3)的任务0种继续运行。在移动之前,系统在对调度程序的初始化过程(sched_init())中,首先对任务0的运行环境进行的设置。这包括人工预先设置好 任务0数据结构各字段的值(include/linux/shed.h),在全局描述符中添入任务0的任务状态段(TSS)描述符和局部描述符表(LDT)的段描述符,并把它们分别加载到任务寄存器tr和局部描述符表寄存器ldtr中。

 

  这里需要强调的是,内核初始化是一个特殊过程,内核初始化代码也即是任务0的代码。从任务0数据结构中设置的初始化数据可知,任务0的代码段和数据段的基址是0,段限长是640KB。而内核代码段和数据段的基地址时0,段限长是16MB,因此任务0的代码段和数据段分别包含在内核代码段和数据段中。内核初始化程序main.c也即是任务0中的代码,只是在移动到任务0之前系统正以内核态特权级0运行着main.c程序。宏move_to_user_mode的功能就是把运行特权级内核态的0级变换到用户态的3级,但是仍然继续执行原来的代码指令流。

 

  在移动到任务0的过程中,宏move_to_user_mode使用了中断返回指令造成特权级改变的方法。该方法的主要思想是在对站中构筑中断返回指令需要的内容,把返回地址的段选择符设置成任务0代码段选择符,其特权级为3。此后执行中断返回指令iret时将导致系统CPU从特权级0跳转到外层的特权级3上运行。参见下图所示的特权级发生变化时中断返回堆栈结构示意图。

       宏move_to_user_mode首先往内核堆栈中压入任务0数据段选择符和内核堆栈指针。然后压入标志寄存器内容。最后压入任务0代码段选择符合执行中断返回后需要执行的下一条指令的偏移位置。该偏移位置是iret后的一条指令处。

  当执行iret指令时,CPU把返回地址送入CS:EIP中,同时探出对站中标志寄存器内容。由于CPU判断出墓地代码段的特权级是3,与当前内核态的0级不同。于是CPU会把堆栈中的堆栈段选择符合堆栈指针弹出到SS:ESP中。由于特权级发生了变化,段寄存器DS,ES,FS和GS的值变得无效,此时CPU会把这些段寄存器清零。因此在执行了iret指令后需要重新加载这些段寄存器。此后,系统就开始特权级3运行在任务0的代码上。所使用的用户态堆栈还是原来在移动之前使用的堆栈。而其内核态堆栈则被指定为其任务数据解噢股所在页面的顶端开始(PAGE_SIZE+(long)&init_task)。由于以后在创建新进程时,需要复制任务0的任务数据结构,包括其用户堆栈指针,因此需要任务0的用户态堆栈在创建任务1(进程1)之前保持"干净"状态。

2、进程创建的执行

  • fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;
  • Linux通过复制父进程来创建一个新进程,那么这就给我们理解这一个过程提供一个想象的框架:
  • 复制一个PCB——task_struct 
1 err = arch_dup_task_struct(tsk, orig);
  • 要给新进程分配一个新的内核堆栈
t= alloc_thread_info_node(tsk, node);
tsk->stack = t;
setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
  • 要修改复制过来的进程数据,比如pid、进程链表等等都要改改吧,见copy_process内部。
  • 从用户态的代码看fork();函数返回了两次,即在父子进程中各返回一次,父进程从系统调用中返回比较容易理解,子进程从系统调用中返回,那它在系统调用处理过程中的哪里开始执行的呢?这就涉及子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,这是在哪里设定的?copy_thread in copy_process
1 *childregs = *current_pt_regs(); //复制内核堆栈
2  childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!
3  
4  p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
5  p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址

3、进程实现

在Linux内核的角度并没有线程的概念,都当做进程来实现。线程仅仅被视为一个与其他进程共享某些资源的进程。对Linux来说,线程只是一种进程间共享资源的手段。 
创建线程,线程的创建和进程的创建相同,同样是调用clone()函数,只不过创建线程需要多传递几个参数来表明共享的资源:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
//参数分别表明父子俩共享地址空间、文件系统资源、文件描述符、信号处理程序,其他与创建进程相同

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

4、进程终止

一个进程终结时,内核必须释放它占有的所有资源,并通知其父进程。 
进程终止的方式有很多种,进程的析构发生在调用exit()之后。

当进程接收到不能处理也不能忽略的信号或异常时,也可能被动的终结。

不管怎么终结,该任务大部分靠do_exit()完成。

进程相关资源释放后,进程进入EXIT_ZOMBIE状态。

现在占用的资源就是内核栈、thread_info结构、task_struct结构。

现在进程存在的唯一目的就是向它的父进程提供信息。

父进程检索到信息后,或者告知内核那是无关信息后,子进程的task_struct结构才会被释放。 
wait()函数族都是通过系统调用wait4()来实现的。

它的动作就是挂起调用它的进程,直到其中的一个子进程退出,此时函数返回该子进程的PID。最终释放进程描述符时会调用release_task()。

三、进程状态转换

1、进程状态

     · R(TASK_RUNNING),可执行状态。

          只有在该状态的进程才可能在CPU上运行,同一时刻可能有多个进程处于可执行状态。 

  • S(TASK_INTERRUPTIBLE),可中断的睡眠状态。

    处于这个状态的进程因为等待某事件的发生(比如等待socket连接、等待信号量),而被挂起。当这些事件发生时,对应的等待队列中的一个或多个进程将被唤醒。一般情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态。

  • D(TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。

    与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的是进程不响应异步信号,无法用kill命令关闭处于TASK_UNINTERRUPTIBLE状态的进程。

  • T(TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态。

    向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态(除非该进程本身处于TASK_UNINTERRUPTIBLE状态而不响应信号)。当进程正在被跟踪时,它处于TASK_TRACED状态。

  • Z(TASK_DEAD - EXIT_ZOMBIE),退出状态。

    进程在退出的过程中,处于TASK_DEAD状态,如果它的父进程没有收到SIGCHLD信号,故未调用wait(如wait4、waitid)处理函数等待子进程结束,又没有显式忽略该信号,它就一直保持EXIT_ZOMBIE状态。只要父进程不退出,这个EXIT_ZOMBIE状态的子进程就一直存在。

  • X(TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁。

    EXIT_DEAD状态是非常短暂的,几乎不可能通过ps命令捕捉到。

     2、进程状态转换

    盗一张图来说明一下进程状态转换的过程: 

 五、进程调度

1、背景: Linux进程是抢占式的。被抢占的进程仍然处于TASK_RUNNING状态,只是暂时没有被CPU运行。进程的抢占发生在进程处于用户态执行阶段,在内核执行时是不能被抢占的。

             为了能让进程有效地使用系统资源,又能使进程有较快的响应时间,就需要对进程的切换调度采用一定的调度策略。在Linux0.11中采用了基于优先级排队的调度策略。

2、调度相关数据结构

1 task_struct
//这个结构体中表示优先级的有3个元素:static_prio,normal_prio, prio。
//分别表示静态(启动时分配)、普通(静态优先级+调度策略)、动态优先级(可临时提高)。
2 struct sched_class
/*

这个结构体提供了通用调度器(核心调度器)和各个调度方法之间的关联。这个结构体包括的操作:

enqueue_task/dequeue_task,操作就绪队列

sched_yield进程放弃对处理器的控制权

check_preemp_cur 用一个新进程来抢占当前进程

pick_next_task选择下一个将要运行的进程

set_curr_task 调度测率发生变化时

task_tick 每次激活周期性调度器时

new_task 用于fork系统调用和调度器之间的联系。

*/

3 struct rq
 /*内核提供了struct rq数据结构用于表示就绪队列,并为系统所有就绪队列声明了runqueues数组中,该数组每个元素对应一个CPU*/
4 struct sched_entity
/*
调度器可以操作比进程更一般的实体,这个结构体中比较重要的元素包括:

load: 调度实体的权重,它与就绪队列中的负荷比作为调度算法的重要参考依据

run_node: 有了它就可以将调度实体放入红黑树

on_rq:是否接受调度

sum_exec_runtime/prev_sum_exec_runtime:记录消耗的CPU时间(注意是物理时间)

vruntim:记录消耗的虚拟时间
*/

3、调度算法(CFS

定义:CFS是英文Completely Fair Scheduler的缩写,即完全公平调度器,负责进程调度。在Linux Kernel 2.6.23之后采用,它负责将CPU资源,分配给正在执行的进程,目标在于最大化程式互动效能,最小化整体CPU的运用。使用红黑树来实现,算法效率为O(log(n))。

原理:调度算法最核心的两点即为调度哪个进程执行、被调度进程执行的时间多久。前者称为调度策略,后者为执行时间。

执行时间:cfs采用当前系统中全部可调度进程优先级的比重确定每一个进程执行的时间片,即

//分配给进程的时间 = 调度周期 * 进程权重 / 全部进程之和。

算法内核实现:

cfs调度算法使用红黑树来实现,其详细内容可以参考维基百科红黑树的介绍。这里简单讲一下cfs的结构。第一个是调度实体sched_entity,它代表一个调度单位,在组调度关闭的时候可以把他等同为进程。每一个task_struct中都有一个sched_entity,进程的vruntime和权重都保存在这个结构中。 
sched_entity通过红黑树组织在一起,所有的sched_entity以vruntime为key(实际上是以vruntime-min_vruntime为key,是为了防止溢出)插入到红黑树中,同时缓存树的最左侧节点,也就是vruntime最小的节点,这样可以迅速选中vruntime最小的进程。

如图(借鉴网上):

两个重要的结构体:

/*完全公平队列cfs_rq*/

struct cfs_rq {
    struct load_weight load;  //运行队列总的进程权重
    unsigned int nr_running, h_nr_running; //进程的个数

    u64 exec_clock;  //运行的时钟
    u64 min_vruntime; //该cpu运行队列的vruntime推进值, 一般是红黑树中最小的vruntime值

    struct rb_root tasks_timeline; //红黑树的根结点
    struct rb_node *rb_leftmost;  //指向vruntime值最小的结点
    //当前运行进程, 下一个将要调度的进程, 马上要抢占的进程, 
    struct sched_entity *curr, *next, *last, *skip;

    struct rq *rq; //系统中有普通进程的运行队列, 实时进程的运行队列, 这些队列都包含在rq运行队列中  
    ...
    };
/*调度实体sched_entity:记录一个进程的运行状态信息*/

struct sched_entity {
    struct load_weight  load; //进程的权重
    struct rb_node      run_node; //运行队列中的红黑树结点
    struct list_head    group_node; //与组调度有关
    unsigned int        on_rq; //进程现在是否处于TASK_RUNNING状态

    u64         exec_start; //一个调度tick的开始时间
    u64         sum_exec_runtime; //进程从出生开始, 已经运行的实际时间
    u64         vruntime; //虚拟运行时间
    u64         prev_sum_exec_runtime; //本次调度之前, 进程已经运行的实际时间
    struct sched_entity *parent; //组调度中的父进程
    struct cfs_rq       *cfs_rq; //进程此时在哪个运行队列中
};

六、浅谈操作系统进程模型

首先操作系统具有多进程的性质,可以形象地把进程理解为某种类型的一个活动,具有程序,输入,输出以及状态。

单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个·进程服务。

七、参考资料
https://blog.csdn.net/yaosiming2011/article/details/44280797
https://wenda.so.com/q/1403756849501892?src=300
https://blog.csdn.net/lyc_stronger/article/details/51999465

 

 

 

 

 

posted @ 2018-04-28 18:51  紫色,风铃  阅读(265)  评论(0编辑  收藏  举报