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

前言:

         这是一篇关于linux操作系统的简单介绍。linux本身不能算是操作系统,只是一个内核,基于linux内核的操作系统有很多,比如流行的android,ubuntu,红旗linux等等。Linux以它的高效性和灵活性著称。它能够在PC计算机上实现全部的Unix特性,具有多任务、多用户的能力。Linux是在GNU公共许可权限下免费获得的,是一个符合POSIX标准的操作系统。Linux操作系统软件包不仅包括完整的Linux操作系统,而且还包括了文本编辑器、高级语言编译器等应用软件。它还包括带有多个窗口管理器的X-Windows图形用户界面,如同我们使用Windows NT一样,允许我们使用窗口、图标和菜单对系统进行操作。

 

.操作系统是怎么组织进程的?

         1.1 进程的概念

                     一个进程是一个程序的一次执行的过程。它和程序不同,程序是静态的,它是一些保存在磁盘上的可执行的代码和数据集合;而进程是一个动态的概念,它是Linux系统的基本的调度单位。一个进程由如下元素组成: 

            1、 程序的读取上下文,它表示程序读取执行的状态。

           2、 程序当前执行目录。

           3、 程序服务的文件和目录。

           4、 程序的访问权限。 

           5、 内存和其他分配给进程的系统资源。

     1.2 进程的创建

          fork()提供了创建进程的基本操作,可以说它是Linux系统多任务的基础。 其实fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建。Linux通过复制父进程来创建一个新进程,那么这就给我们理解这一个过程提供一个想象的框架:

  • 复制一个PCB——task_struct

        

err = arch_dup_task_struct(tsk, orig);
  • 给新进程分配一个新的内核堆栈
ti = alloc_thread_info_node(tsk, node);
tsk->stack = ti;
setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
  • 修改复制过来的进程数据,比如pid、进程链表等等
*childregs = *current_pt_regs(); //复制内核堆栈
 childregs->ax = 0; //子进程的fork返回0
p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址

              1.3 进程的组织

                  1.3.1等待队列  

        等待队列在内核中有很多用途,尤其用在中断处理,进程同步及定时。等待队列还实现了在时间上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。等待队列数据结构为双向链表,其元素包括指向进程描述符的指针。列头是一个类型为wait_queue_head_t的数据结构:

 

struct _ _wait_queue_head {
      spinlock_t lock;
      struct list_head task_list;
};
typedf struct_ _wait_queue_head wait_queue_head_t;

 

        等待队列是由中断处理程序和主要内核函数修改的,因此必须对其双向连表进行保护以免对其进行同时访问,因为同时访问会导致不可预测的后果。同步是通过等待队列头中的Lock自旋锁打到的。task_list字段是等待进程聊表的头。

 二.进程状态如何转换?

         

 

                   ◆运行状态(TASK_RUNNING)

 

                            指正在被CPU运行或者就绪的状态。这样的进程被成为runnning进程。运行态的进程可以分为3种情况:内核运行态、用户运行态、就绪态。

 

                   ◆可中断睡眠状态(TASK_INTERRUPTIBLE)

 

                             处于等待状态中的进程,一旦被该进程等待的资源被释放,那么该进程就会进入运行状态。

 

                   ◆不可中断睡眠状态(TASK_UNINTERRUPTIBLE)

 

                             该状态的进程只能用wake_up()函数唤醒。

 

                   ◆暂停状态(TASK_STOPPED)

 

                             当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。

 

                   ◆僵死状态(TASK_ZOMBIE)

 

                              当进程已经终止运行,但是父进程还没有询问其状态的情况。

 

三.进程是如何调度的?

  •        在 Linux中,进程的运行时间不可能超过分配给他们的时间片,他们采用的是 抢占式多任务处理,所以进程之间的挂起和继续运行无需彼此之间的协作。在一个如 linux这样的多任务系统中,多个程序可能会竞争使用同一个资源,在这种情况下,我们认为,执行短期的突发性工作并暂停运行以等待输入的程序,要比持续占用处理器以进行计算或不断轮询系统以查看是否有输入到达的程序要更好。我们称表现好的程序为 nice程序,而且在某种意义上,这个nice 是可以被计算出来的。 操作系统根据进程的 nice值来决定它的优先级,一个进程的nice值默认为0并将根据这个程序的表现不断变化。长期不间断运行的程序的优先级一般会比较低。进程状态这个是最优先考虑的,也就是说优先级最高的。下面是linux中进程的状态:

                  TASK_RUNNING:就绪状态,得到CPU就可以运行。

                  TASK_INTERRUPTIBLE:浅度睡眠,资源到位或者受到信号就会变成就绪态。

                  TASK_UNINTERRUPTIBLE:深度睡眠,资源到位就会进入就绪态,不响应信号。

                  TASK_ZOMBIE:僵死态,进程exit后。

                  TASK_STOPPED:暂停态,收到SIG_CONT信号进入就绪态。

 

  •            同样的linux也给出了三种的调度策略三种调度策略: SCHED_OTHER、SCHED_FIFO 和 SCHED_RR。

                  SCHED_OTHER:阻塞和上述相同;被抢占后,其动态优先级会被重新计算,并安排到新的队列尾部;时间片耗尽后,任务不再存在于活动任务队列中,而是被放到过期任务队列中。必须等到过期任务队列切换为活动任务队列,才可能被重新调度。

                  SCHED_FIFO:如果因为阻塞切换出去,则进入阻塞态,不再继续态列表中,因此不涉及到调度的问题;如果是因为出让CPU或被抢占切换出去,则直接回到队列头中,后面可继续执行。

                  SCHED_RR:阻塞和被抢占时的情形与SCHED_FIFO相同;如果是因为时间片耗尽造成的切换,则任务回到队列尾部。

 

  •             并且每个CPU拥有独立的队列,存在专用的负载均衡算法,可实现多CPU之间的进程调配 。

                    系统的所有就绪队列都在runqueues数组中,数组元素个数与cpu个数相关

DECLARE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);

                  可执行队列的数据结构为struct runqueue,定义在sched.c中。

struct runqueue {

spinlock_t
lock; /* 保护队列锁 */ unsigned long nr_running; /* 可运行的任务数 */ unsigned long expired_timestamp; /* 队列最近被换出时间 */ unsigned long nr_uninterruptible; /* 处在不可打断睡眠的任务数 */ prio_array_t *active, *expired, arrays[2]; /* 优先级数组相关 */ … }

 

                 运行队列容纳了系统中所有可以运行的进程,它是一个双向循环队列。该队列通过task_struct结构中的两个指针run_list链表来维持。队列的标志有两个:一个是“空进程”idle_task、一个是队列的长度。有两个特殊的进程永远在运行队列中待着:当前进程和空进程。前面我们讨论过,当前进程就是由cureent指针所指向的进程,也就是当前运行着的进程,但是请注意,current指针在调度过程中(调度程序执行时)是没有意义的,为什么这么说呢?调度前,当前进程正在运行,当出现某种调度时机引发了进程调度,先前运行着的进程处于什么状态是不可知的,多数情况下处于等待状态,所以这时候current是没有意义的,直到调度程序选定某个进程投入运行后,current才真正指向了当前运行进程;空进程是个比较特殊的进程,只有系统中没有进程可运行时它才会被执行,Linux将它看作运行队列的头,当调度程序遍历运行队列,是从idle_task开始、至idle_task结束的,在调度程序运行过程中,允许队列中加入新出现的可运行进程,新出现的可运行进程插入到队尾,这样的好处是不会影响到调度程序所要遍历的队列成员,可见,idle_task是运行队列很重要的标志。

另一个重要标志是队列长度,也就是系统中处于可运行状态(TASK_RUNNING)的进程数目,用全局整型变量nr_running表示,在/kernel/fork.c中定义如下:

int nr_running=1

 

若 nr_running为0,就表示队列中只有空进程。在这里要说明一下:若nr_running为0,则系统中的当前进程和空进程就是同一个进程。但是Linux会充分利用CPU而尽量避免出现这种情况。

四.谈谈自己对该操作系统进程模型的看法

        在linux操作系统中,往往是就绪的进程数目很多,在某一时刻必定有进程在就绪态等待处理器使用时间。Linux系统的特性在于可以并发的交互执行多个进程。主要分为非抢占式任务系统和抢占式任务系统。在抢占式任务系统中,调度程序决定了一个进程的运行以便其它进程可以分享到处理器使用时间。抢占是调度程序强制挂起某一个进程的行为。进程从被允许运行到被强占之前可以运行的时间成为时间片。时间片的管理属于进程调度的一部分。有效的管理时间片可以避免个别优先级较高的程序独占处理器。对非抢占的Linux,除非正在运行的进程自己主动停止运行,否则会一直占据处理器使用权。如果该进程出现错误,则整个系统就会崩溃。让步是进程主动放弃处理的行为。同时,进程的优先级可以被设定,而且是动态的。

 

 

 

posted @ 2018-05-01 10:56  魏人民  阅读(133)  评论(0编辑  收藏  举报