代码改变世界

第一次作业:深入源码分析进程模型

2018-05-08 19:55  爱灰灰  阅读(162)  评论(0编辑  收藏  举报

 1.作业内容

      挑选一个开源的操作系统,深入源码分析其进程模型,具体包含如下内容:

  • 操作系统是怎么组织进程的
  • 进程状态如何转换(给出进程状态转换图)
  • 进程是如何调度的
  • 谈谈自己对该操作系统进程模型的看法

2.进程描述 

  Linux下的进程结构
  Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。
  Linux中的进程中包含3个段,分别为“数据段”、  “代码段”和“堆栈段
  “数据段”存放的是全局变量、常数以及动态数据分配的数据空间根据存放的数据,数据段又可以分成普通数据段(包括可读可写/只读数据段,存放静态初始化的全局变量或常量)、BSS数据段(存放未初始化的全局变量) 以及堆(存放动态分配的数据)
  “代码段”存放的是程序代码的数据。
  “堆栈段”存放的是子程序的返回地址、子程序的参数以及程序的局部变量。如下图所示。


  在Linux系统中,进程的执行模式分为用户模式和内核模式。如果当前运行的是用户程序、应用程序或内核程序之外的系统程序,那么对应程序就在用户模式下进行; 如果在用户程序的执行过程中出现系统调用或者发生中断事件,那么就要运行操作系统(内核) 程序,进程模式就变成内核模式。在内核模式下运行的程序可以执行机器的特权指令,  而且此时运行的程序不受用户的干扰,即使是root 用户也不能干扰内核模式下进程的运行。
  用户进程可以在用户模式下运行,也可以在内核模式下运行,如下图所示
 
 
   进程表中的task(0)和task(1)在Linux[ 中是特别的进程。task(0)即init_task,是系统启动时第一个产生的进程,扮演个特殊的角色。task(1)即PID为1的进程,是Linux系统中第一个“真正的”进程。因为它通常执行init程序,所以也成为init进程。这两个进程在核心中需要一直存在,因此它们的进程号(0和1)只负责“无主的”系统时间的使用,即空转进程。因此在调度等过程中需1描进程表链,它通常被跳过。

  在Linux中进程又称为任务。进程表是由NR_TASKS个task结构组成的静态数组:
  struct task_struct *task[NR_TASKS] ;
  每个进程在进程表中占有一项(一个task_struct 结构)。在该表中,所有进程通过一个双向环形链相连,链头由外部变量init_task指定:
  struct task_struct ini t_task;
  init.task对应于系统的第一个任务INIT TASK,它在进程表链被扫描时通常被跳过。当前进程由变量current 指定:
  struct task_struct current;

  必运行态:  该任务是“活的”,而且正在非特权的用户方式下运行。
  必中断处理状态:当硬件发出一一个中断处理信号时(可能是用户键入一
  个字符,或时钟每隔10ms发出一次中断等),中断处理例程被激活
  必系统调用期间: 系统调用通过软件中断启动。一个系统调用可能将相
  应程序进程挂起等待- 一个事件。
  必等待态: 进程正等待- 一个外部事件。只有在这个事件发生后,进程才
  能继续工作。
  必从系统调用返回: 每个系统调用后自动进入该状态,每个慢中断处理
  完毕后也自动进入该状态。在该状态下,要查看是否需要调用程序是否有信号要处理。调用程序可能将该进程切换为就绪态,同时激活另一进程。
  必就绪态:.处于此状态的进程正在竞争处理机,但处理机正在被另- 一个
  进程占用。

  amd:  自动安装NFS (网络文件系统) 守侯进程。apmd: 高级电源管理。
  Arpwatch:  记录日志并构建一个在LAN接口上看到的以太网地址和IP地址对数据库。  Autofs:  自动安装管理进程automount,与NFS相关
  依赖于NIS。
  Bootparamd:引导参数服务器,为LAN 上的无盘工作站提供引导所需的相关信息,用于无盘客户端,通常都不需要。crond:  Linux 下的计划任务。
  Dhcpd: 启动- 一个DHCP (动态IP地址分配) 服务器。
  Gated:  网关路由守候进程,使用动态的OSPF路由选择协议。Httpd:  WEB 服务器。
  Inetd:  支持多种网络服务的核心守候程序。Innd: Usenet 新闻服务器。
  Linuxconf:  允许使用本地WEB服务器作为用户接口来配置机器。Lpd:  打印服务器。

  M ars-nwe :  Netware 文件和打印服务器。Mcserv:  Midnight命令文件服务器。nam ed :  DNS服务器。
  netfs:  安装NFS、Samba和NetWare网络文件系统。network: 激活已配置网络接[ 1的脚本程序。nfs:  打开NFS服务。
  nscd: nscd(Name Switch Cache daemon) 服务器,用于NIS的一个支持服务,它高速缓存用户口令和组成成员关系。
  portmap: RPC portmap管理器,与inet d 类似,它管理基于RPC服务的连接。postgresql :一种SQL数据库服务器。
  routed:  路由守候进程,  使用动态RIP路由选择协议。
  rstatd:  个为LAN 上的其它机器收集和提供系统信息的守候程序。
  ruserd: 远程用户定位服务,这;是一一个基于RPC的服务,它提供关于当前记录到LAN上一个机器日志中的用户信息。
  rwalld: 激活rpc.rwal 1服务进程,这是- 一项基于RPC的服务,允许用户给每个主册到LAN机器上的其他终端写消息。
  rwhod: 激活rwhod服务进程,它支持LAN的rwho 和rupt ime服务。
  sendmai 1: 邮件服务器sendmai 1,如果不需要接收或转发电子邮件应关闭,此时仍可发送电子邮件。
  sound:  保存声卡设置。
  smb:  Samba 文件共享/打印服务。snmpd :  本地简单网络管理候进程。squid:  激活代理服务器squid。
  syslog: 一个让系统引导时起动syslog和klogd系统E 志守候进程的脚本。
  xfs:  X Window字型服务器,为本地和远程X服务器提供字型集。xntpd:  网络时间服务器。
  ypbind:  为NIS (网络信息系统) 客户机激活ypbind服务进程,如果系统运行NIS服务器,则必需此服务。
  yppasswdd: NIS[ ]令服务器,如果系统运行NIS服务器,则必需此服务。
  ypserv:  NIS.主服务器。gpm :  鼠标的管理。
  identd: AUTH服务,在提供用户信息方面与finger 类似。


1.慢中断处理最初保存现场时,需要保存所有寄存器的值。而快中断处理最初保存现场时,只需要保存那些被常规C函数修改的寄存器的值(但如果在中断处理过程用到汇编代码,则其余寄存器也必须保存和恢复)。
2.在慢中断处理时,通常不屏蔽其他中断。而快中断处理时,通常要屏蔽其他所有中断(除特别说明)
3.慢中断处理完毕后,通常不立即返回被中断的进程,而是调用程序
  调用程序的调度结果不一定是被中断的进程,因此这种情况称为抢先式调度或抢先式中断。而快中断处理完毕后,通常恢复现场返回被中断的进程继续执行,称为非抢占式调度或非抢占式中断。慢中断是最常见的中断。而快中断则通常用于短时间的、不太复杂的中断处理任务。

      每一个普通进程都有一个静态优先级。这个值会被调度器用来与作为参考来调度进程。在内核中调度的优先级的区间为[100,139],数字越低,优先级越高。一个新的进程总是从它的父进程继承此值。从进程管理篇(一)中了解到每个进程都有分配时间片。那么时间是如何分配的呢? 时间片的计算公式如下(摘自《UnderstandingThe LinuxKernel Version3》) :
  base time q anti m  | 140- static prionity) x 20 it staticprioritys 120  (1)
  (in mill iseconds)  l( 140-static priority)x 5if static priority2 120

  由此可见时间片的分配只有静态优先级相关。静态优先级越高,时间片分配的越多。系统在实际的调度过程中不仅要考虑静态优先级,也要考虑进程的属性。
  系统调度时,会计算进程的动态优先级。系统会严格按照动态优先级高低的顺序安排进程执行。动态优先级高的进程进入非运行状态
  或者时间片消耗完毕才会轮到动态优先级较低的进程执行。动态优先级的计算主要考虑两个因素: 静态优先级,进程的平均睡眠时间也即bonus。计算公式如下:  动态优先级= max (100,min( 静态优先级一奖励+ 5,139))。

     每一个实时进程都会与一个实时优先级相关联。实时优先级在1到99之间。不同与普通进程,系统调度时,实时优先级高的进程总是先于优先级低的进程执行。知道实时优先级高的实时进程无法执行。实时进程总是被认为处于活动状态 如果有数个优先级相同的实时进程,那么系统就会按照进程出现在队列上的顺序选择进程。假设当前CPU运行的实时进程A的优先级为a,  而此时有个优先级为b的实时进程B进入可运行状态,那么只要b<a,系统将中断A的执行,而优先执行B,直到B无法执行(无论A,B为何种实时进程)。不同调度策略的实时进程只有在相同优先级时才有可比性:意味着只有当前进程执行完毕才会轮到其他进对于FIFO的进程,程执行。由此可见相当霸道。对于RR的进程。一旦时间片消耗完毕,则会将该进程置于队列的然后运行其他相同优先级的进程,如果没有其他相同优先级的未尾,进程,则该进程会继续执行。
 

3.代码段:

得到PID和PPID:
int main(void) {

printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
return EXIT_SUCCESS;
}

得到登陆的用户号、用户名,宿主目录信息:

char *login = getlogin();
struct passwd *ps = getpwnam(login);
printf("user name = %s\n",ps->pw_name);
printf("uid = %d\n",ps->pw_uid);
printf("home dir = %s\n",ps->pw_dir);

fork 函数:

int main(void) {
printf("begin\n");
pid_t child = fork();
if(child == -1)
{
return -1;
}
if(child == 0)
{
printf("is child\n");
}else
{
printf("is parent\n");
}
printf("end\n");
return EXIT_SUCCESS;
}

进程描述符

 就是用于描述一个进程的结构体,每个进程有且只有一个进程描述符,它里面包含了这个进程相关的所有信息。

struct task_struct {

    ......

    /* 进程状态 */
    volatile long state;
    /* 指向内核栈 */
    void *stack;
    /* 用于加入进程链表 */
    struct list_head tasks;
    ......

    /* 指向该进程的内存区描述符 */
    struct mm_struct *mm, *active_mm;

    ........

    /* 进程ID,每个进程(线程)的PID都不同 */
    pid_t pid;
    /* 线程组ID,同一个线程组拥有相同的pid,与领头线程(该组中第一个轻量级进程)pid一致,保存在tgid中,线程组领头线程的pid和tgid相同 */
    pid_t tgid;
    /* 用于连接到PID、TGID、PGRP、SESSION哈希表 */
    struct pid_link pids[PIDTYPE_MAX];

    ........

    /* 指向创建其的父进程,如果其父进程不存在,则指向init进程 */
    struct task_struct __rcu *real_parent;
    /* 指向当前的父进程,通常与real_parent一致 */
    struct task_struct __rcu *parent;

    /* 子进程链表 */
    struct list_head children;
    /* 兄弟进程链表 */
    struct list_head sibling;
    /* 线程组领头线程指针 */
    struct task_struct *group_leader;

    /* 在进程切换时保存硬件上下文(硬件上下文一共保存在2个地方: thread_struct(保存大部分CPU寄存器值,包括内核态堆栈栈顶地址和IO许可权限位),内核栈(保存eax,ebx,ecx,edx等通用寄存器值)) */
    struct thread_struct thread;

    /* 当前目录 */
    struct fs_struct *fs;

    /* 指向文件描述符,该进程所有打开的文件会在这里面的一个指针数组里 */
    struct files_struct *files;

    ........

  /* 信号描述符,用于跟踪共享挂起信号队列,被属于同一线程组的所有进程共享,也就是同一线程组的线程此指针指向同一个信号描述符 */
  struct signal_struct *signal;
  /* 信号处理函数描述符 */
  struct sighand_struct *sighand;

  /* sigset_t是一个位数组,每种信号对应一个位,linux中信号最大数是64
   * blocked: 被阻塞信号掩码
   * real_blocked: 被阻塞信号的临时掩码
   */
  sigset_t blocked, real_blocked;
  sigset_t saved_sigmask;    /* restored if set_restore_sigmask() was used */
  /* 私有挂起信号队列 */
  struct sigpending pending;


    ........
}

 

 组织进程

所有处于TASK_RUNNING状态的进程都会被放入CPU的运行队列,它们有可能在不同CPU的运行队列中。 系统没有为TASK_STOPED、EXIT_ZOMBIE和EXIT_DEAD状态的进程建立专门的链表,因为处于这些状态的进程访问比较简单,可通过PID和通过特定父进程的子进程链表进行访问。所有TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE都会被放入相应的等待队列,系统中有很多种等待队列,有些是等待磁盘操作的终止,有些是等待释放系统资源,有些是等待时间经过固定的间隔,每个等待队列它的唤醒条件不同,比如等待队列1是等待系统释放资源A的,等待队列2是等待系统释放资源B的。因此,等待队列表示一组睡眠进程,当某一条件为真时,由内核唤醒这条等待队列上的进程。我们看看内核中一个简单的sleep_on()函数:

/* wq为某个等待队列的队列头 */
void sleep_on (wait_queue_head_t *wq)
{
    /* 声明一个等待队列结点 */
    wait_queue_t wait;

    /* 用当前进程初始化这个等待队列结点 */
    init_waitqueue_entry (&wait, current);

    /* 设置当前进程状态为TASK_UNINTERRUPTIBLE */
    current->state = TASK_UNINTERRUPTIBLE;

    /* 将这个代表着当前进程的等待队列结点加入到wq这个等待队列 */
    add_wait_queue (wq, &wait);

    /* 请求调度器进行调度,执行完schedule后进程会被移除CPU运行队列,只有等待队列唤醒后才会重新回到CPU运行队列 */
    schedule ();

    /* 这里进程已经被等待队列唤醒,重新移到CPU运行队列,也就是等待的条件已经为真,唤醒后第一件事就是将自己从等待队列wq中移除 */
    remove_wait_queue (wq, &wait);  
}