关于linux操作系统中进程相关问题的学习笔记

关于linux操作系统中进程相关问题的学习笔记

1.摘要

  进程的经典定义是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中(contest)中。上下文是由程序运行正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符合的集合。在本次学习笔记中,我是以linux为例,学习了以下三个方面的知识:操作系统如何组织进程、进程状态如何转换以及进程是如何调度的。在最后我还谈了下自己对操作系统进程模型的一些学习心得。

2.操作系统如何组织进程

进程是由程序、数据和进程快PCB(Process Control Block)组成。进程控制块PCB是进程存在唯一标识,系统通过PCB的存在而感知进程的存在.当创建一个进程时,实际上是建立一个PCB。当进程消失时,实际上是撤销PCB。在linux中,每个进程中的PCB用一个名为task struct的结构体来表示,定义在include/linux/sched.h中。

1 struct task_struct {
2     pid_t pid;
3     pid_t tgid;
4
5     /* PID/PID hash table linkage. */
6     struct pid_link pids[PIDTYPE_MAX];   <span style="color:#ff0000;"> </span>//一个进程ID可能是多种身份,比如Session ID,进程组ID, 进程ID,所以指向多个pid节点
7     struct list_head thread_group;
8 }  

linux可以运行的进程数量可达到成千上万个(用ps 命令可查看当前进程),而这些进程又可能处于不同的状态,因此需要操作系统来管理组织它们。linux采用了以下几种方式来组织进程:

2.1哈希表

哈希表是进行快速查找的一种有效的组织方式。 L inux 在进程中引入的哈希表叫做pidhash,在include/linux/sched.h中,定义如下:

stru ct task stru ct*p idhash[ PIDHASH_SZ] ;

PIDHASH SZ 在inc lude /linux /sched. h 中定义, 其值为1024.

系统根据进程的进程号求得hash值, 加到hash表中:

#define p id hash fn(x)((((x) >>8)∧ (x))&(PIDHASH_SZ -1))

其中, PIDHASH_SZ 是表中元素的个数, 表中的元素是指向task_struct结构体的指针。pid_hashfn为哈希函数,将进程的pid转换为表的索引,通过该函数,可以将进程的pid均匀地散列在它们的域中。

函数代码如下:

#define pid_hashfn(nr, ns)  \
    hash_long((unsigned long)nr + (unsigned long)ns, pidhash_shift)
static struct hlist_head *pid_hash;
static unsigned int pidhash_shift = 4;  

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
    struct upid *pnr;  

    hlist_for_each_entry_rcu(pnr,
            &pid_hash[pid_hashfn(nr, ns)], pid_chain)
        if (pnr->nr == nr && pnr->ns == ns)
            return container_of(pnr, struct pid,
                    numbers[ns->level]);  

    return NULL;
}  

如果知道进程号, 可以通过hash表很快地找到该进程,,查找函数如下:

struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
    struct task_struct *result = NULL;
    if (pid) {
        struct hlist_node *first;
        first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
                          lockdep_tasklist_lock_is_held());
        if (first)         //pid中的task[type] 与task_struct.pid[type].node  指向的是同一个节点
            result = hlist_entry(first, struct task_struct, pids[(type)].node);  <span style="color:#ff0000;"> </span>//node实体在task_struct结构中,所以可以利用first指针得到task_struct结构体指针
    }
    return result;
}

2.2双向循环链表

  哈希表的主要作用是根据进程的pid可以快速找到对应的进程,但它没有反映创建的顺序,也无法反映进程之间的亲属关系,而双向循环链表可以弥补这一弱势。

(图1,图片来源:http://blog.chinaunix.net/uid-27033491-id-3233511.html)

其对应的结构体是:

struct task_struct {
   ...;
    struct list_head tasks;
   ...;
};

struct list_head {
    struct list_head *next,*prev;
};

2.3运行队列

  当内核要寻找一个新的进程在CPU运行时,一般只考虑那些处于可运行状态的进程,因为查找整个进程链表效率是很低的, 所以引入了可运行状态进程的双向循环链表, 也叫运行队列。运行队列容纳了系统中所有可以运行的进程, 它是一个双向循环队列, 该队列通过task _truc t结构中的两个指针run_list链表来维护。 队列的标志有两个:一个是“空进程” id le_task,一个是队列的长度。空进程是一个比较特殊的进程, 只有系统中没有进程可运行时它才会被执行, L inux 将它看作运行队列的头, 当调度程序遍历运行队列时, 是从idle_task开始、到idle_task结束的。

2.4等待队列

运行队列链表将所有状态为TASK_RUNNING 的进程组织在一起.在一起。 将所有状态为TASK _INTERRUPT IBLE和TASK_UNINTERRUPTIBLE的进程组织在一起而形成的远程链表称为等待队列。进程必须经常等待某些事件的发生, 等待队列实现在事件上的条件等待, 希望等待特定事件的进程将自己放进合适的等待队列, 并放弃控制权。 等待队列表示一组睡眠的进程, 当条件满足时, 由内核将它们唤醒。
等待队列由循环链表实现:

struct __wait_queue {
    unsigned int flags;
#define WQ_FLAG_EXCLUSIVE   0x01
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
}; 

(图2,图片来源:https://blog.csdn.net/silent123go/article/details/52599210)

3.进程状态如何转换

Linux 系统中的进程有几种关键的状态,他们分别是可执行状态(TASK_RUNNING),可中断的睡眠状态(TASK_INTERRUPTIBLE),不可中断的睡眠状态(TASK_UNINTERRUPTIBLE),暂停(TASK_STOPPED),跟踪状态(TASK_TRACED),僵死状态(EXIT_ZOMBIE)和退出状态(TASK_DEAD)等。各种状态之间的关系如图3所示。这些状态主要是依据进程与CPU 之间的关系来划分的,为的是操作系统内核能对CPU 和进程进行有效地管理。

(图3,图片来源:杨兴强,刘翔鹏,刘毅.Linux进程状态演化过程的图形学表示[J].系统仿真学报,2013,25(10):2444-2448)

状态切换实在contest_switch中实现的,其函数代码如下:

static inline void
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{
     struct mm_struct *mm, *oldmm;

     prepare_task_switch(rq, prev, next);
     mm = next->mm;
     oldmm = prev->active_mm;
     /*
      * For paravirt, this is coupled with an exit in switch_to to
      * combine the page table reload and the switch backend into
      * one hypercall.
      */
     arch_enter_lazy_cpu_mode();

     //task->mm 为空.则是一个内核线程
     if (unlikely(!mm)) {
         //内核线程共享上一个运行进程的mm
         next->active_mm = oldmm;
         //增加引用计数
         atomic_inc(&oldmm->mm_count);
         enter_lazy_tlb(oldmm, next);
     } else
         //如果是用户进程,则切换运行空间
         switch_mm(oldmm, mm, next);

     //如果上一个运行进程是内核线程
     if (unlikely(!prev->mm)) {
         //赋active_mm为空.
         prev->active_mm = NULL;
         //更新运行队列的prev_mm成员
         rq->prev_mm = oldmm;
     }
     /*
      * Since the runqueue lock will be released by the next
      * task (which is an invalid locking op but in the case
      * of the scheduler it‘s an obvious special-case), so we
      * do an early lockdep release here:
      */
#ifndef __ARCH_WANT_UNLOCKED_CTXSW
     spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endif

     /* Here we just switch the register state and the stack. */
     //切换进程的执行环境
     switch_to(prev, next, prev);

     barrier();
     /*
      * this_rq must be evaluated again because prev may have moved
      * CPUs since it called schedule(), thus the ‘rq‘ on its stack
      * frame will be invalid.
      */

     //进程切换之后的处理工作
     finish_task_switch(this_rq(), prev);
}

4.进程如何调度
  

  Linux系统的线程是内核线程,所以Linux系统的调度是基于线程的,而不是基于进程的。

为了实行调度,LInux系统将线程区分为三类:

(1)实时先入先出。
    (2)实时轮转。
  (3)分时。
    实时先入先出线程具有最高优先级,它不会被其它进程抢占。实时轮转线程与实时先入先出进程基本相同,只是每个实时轮转线程都有一个时间量,时间到了之后就可以被抢占。在系统内部,实时线程的优先级从0~99,0是实时线程的最高优先级,99是实时线程的最低优先级。传统的非实时线程形成的单独的类并由单独的算法进行调度,这样可以使非实时线程不与实时线程竞争资源。在系统内部,这些线程的优先级从100-139.也就是说,Linux系统包含140个不同优先级(包括实时和非实时任务)。就像实时轮转线程一样,Linux系统根据非实时线程的要求以及它们的优先级分配CPU时间片。

这里主要学习两个调度算法:Linux O(1)调度器(O(1) scheduler)和完全公平调度器(Compleetely Fair Scheduler,CFS).

4.2 Linux O(1)调度器

schedule()是实现进程调度的主要函数,并负责完成进程切换工作.其用于确定最高优先级进程的代码非常快捷高效,它在/kernel/sched.c中的定义如下

 1 task_t*prev,*next;
 2 runqueue_t*rq;
 3 prio_array_t*array;
 4 intidx;preempt_disable();
 5 prev=current;//Linux2.6内核支持抢占,所以在对队列操作时需要设置为不可抢占rq=this_rq();
 6 array=rq->active;
 7 if(unlikely(!array->nr_active))
 8 {rq->active=rq->expired;
 9 rq->expired=array;
10 array=rq->active;}.

这段代码的作用是执行两个数组(活动数组rq->active和过期数组rq->expired)的切换.判断活动数组中如果没有进程了,则通过指针操作来切换两个数组.之前在过期数组中的进程时间片已经被计算好了.所以在两个数组切换后,过期数组中的进程都变为活动进程,交换数组的时间就是交换指针的时间.这种交换就是O(1)调度算法的核心.O(1)调度算法不需要从头到尾一个一个地对进程进行时间片的计算,而是通过很简单的数组切换实现进程的切换,解决了之前算法中效率低下的弊端.该过程可用图4表示.

(图4,图片来源:张永选,姚远耀.Linux2.6内核O(1)调度算法剖析[J].韶关学院学报,2009,30(06):5-9.)

有了活动数组,并且各个进程都按优先级排好队等待被调度,继而就要选择候选进程了:

1  idx=sched_find_first_bit(array->bitmap);
2  queue=array->queue+idx;
3 next=list_entry(queue->next,task_t,run_list);
4 if(unlikely(next->prio!=new_prio)){dequeue_task(next,array);
5 next->prio=new_prio;enqueue_task(next,array);}
6 elserequeue_task(next,array);

首先,要在活动数组中的索引位图里找到第一个被设置的优先级位,这里通过sched_find_first_bit函数来实现.如前所述,该函数通过汇编指令从进程优先级由高到低的方向找到第一个为1的位置idx.因为优先级的个数是个定值,所以查找时间恒定,并不受系统到底有多少可执行进程的影响.这是Linux2.6内核实现O(1)调度算法的关键之一.此外,Linux对它支持的每一种体系结构都提供了对应的快速查找算法,以保证对位图的快速查找.很多体系结构提供了find-first-set指令,这条指令对指定的字操作(在Intelx86体系结构上,这条指令叫做bsfl.在IBMPPC上,cntlzw用于此目的).在这些系统上,找到第一个要设置的位所花的时间至多是执行这条指令的两倍,这也在很大程度上提高了调度算法的效率.sched_find_first_bit函数找到第一个被设置的优先级位后,再找到该优先级对应的可运行进程队列,接着找到该队列中的第一个进程,最后把找到的进程插入运行队列中.整个过程如下图5所示.图5中的网格为140位索引位图,queue[7]为优先级为7的就绪进程链表..

1 if(likely(prev!=next)){prev=context_switch(rq,prev,next);}
2 elsespin_unlock_irq(&rq->lock);

如果候选进程不是当前运行进程,则需要进行进程切换.反之,仅仅释放之前对运行队列所加的锁.

(图5,图片来源:张永选,姚远耀.Linux2.6内核O(1)调度算法剖析[J].韶关学院学报,2009,30(06):5-9.)

4.2 CFS算法

CFS的主要思想是使用一颗红黑树作为调度队列的数据结构。
  第一个是调度实体sched_entity,它代表一个调度单位,在组调度关闭的时候可以把他等同为进程。每一个task_struct中都有一个sched_entity,进程的vruntime和权重都保存在这个结构中。那么所有的sched_entity怎么组织在一起呢?红黑树。所有的sched_entity以vruntime为key(实际上是以vruntime-min_vruntime为key,是为了防止溢出,反正结果是一样的)插入到红黑树中,同时缓存树的最左侧节点,也就是vruntime最小的节点,这样可以迅速选中vruntime最小的进程。注意只有等待CPU的就绪态进程在这棵树上,睡眠进程和正在运行的进程都不在树上。

(图6:红黑树,图片来源:https://www.cnblogs.com/tianguiyu/articles/6091378.html)

CFS调度算法可以总结如下:该算法总是优先调度那些使用CPU时间最少的任务,通常是在树中最左边节点上的任务。CFS会周期性地根据任务已经停止运行的时间,递增它的虚拟运行时间值,并将这个值与最左边的值进行比较,如果正在运行的任务仍具有较小的虚拟运行时间值,那么它将继续运行,否则,它将插入到红黑树的适当位置,并且CPU将执行新的最左边节点上的任务。

代码如下(函数):

1 struct sched_class { /* Defined in 2.6.23:/usr/include/linux/sched.h */ struct sched_class *next;
2 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup);
3 void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep);
4  void (*yield_task) (struct rq *rq, struct task_struct *p);
5 void (*check_preempt_curr) (struct rq *rq, struct task_struct *p);
6  struct task_struct * (*pick_next_task) (struct rq *rq);
7 void (*put_prev_task) (struct rq *rq, struct task_struct *p);
8 unsigned long (*load_balance) (struct rq *this_rq, int this_cpu, struct rq *busiest, unsigned long max_nr_move, unsigned long max_load_move, struct sched_domain *sd, enum cpu_idle_type idle, int *all_pinned, int *this_best_prio); void (*set_curr_task) (struct rq *rq);
9 void (*task_tick) (struct rq *rq, struct task_struct *p); void (*task_new) (struct rq *rq, struct task_struct *p); };

函数描述

enqueue_task:当某个任务进入可运行状态时,该函数将得到调用。它将调度实体(进程)放入红 黑树中,并对 nr_running 变量加 1。

dequeue_task:当某个任务退出可运行状态时调用该函数,它将从红黑树中去掉对应的调度实体, 并从 nr_running 变量中减 1。

yield_task:在 compat_yield sysctl 关闭的情况下,该函数实际上执行先出队后入队;在这种情况 下,它将调度实体放在红黑树的最右端。

check_preempt_curr:该函数将检查当前运行的任务是否被抢占。在实际抢占正在运行的任务之 前,CFS 调度程序模块将执行公平性测试。这将驱动唤醒式(wakeup)抢占。

pick_next_task:该函数选择接下来要运行的最合适的进程。

load_balance:每个调度程序模块实现两个函数,load_balance_start() 和 load_balance_next(), 使用这两个函数实现一个迭代器,在模块的 load_balance 例程中调用。内核调度程序使用这种方 法实现由调度模块管理的进程的负载平衡。

set_curr_task:当任务修改其调度类或修改其任务组时,将调用这个函数。

task_tick:该函数通常调用自 time tick 函数;它可能引起进程切换。这将驱动运行时(running) 抢占。

task_new:内核调度程序为调度模块提供了管理新任务启动的机会。CFS 调度模块使用它进行组调 度,而用于实时任务的调度模块则不会使用这个函数。

5.我的一点学习心得

1.linux的线程调度是基于线程的,线程切换不必调用系统核心;因此调度过程是基于用户程序的,就可以针对用户程序业务逻辑选择更好的调度算法;

2.不同的算法对系统性能的影响也是不一样的,Linux的发展伴随着算法的逐渐优化;

3.进程调度是Linux操作系统的核心功能,了解进程的代码可更好去学习linux操作系统;同时进程的管理是一种复杂的并发程序设计,需要考虑到很多因素,并且它还是一个开源的操作系统,这些给我们学习带来了非常大的价值性和便利性。

6.参考资料

注:题目中已经给出的引用地址此处不再重复列出。

[1]殷联甫,沈士根,郭步.Linux进程结构及组织方式研究[J].计算机应用与软件,2005(11):61-63+143

[2]https://blog.csdn.net/bysun2013/article/details/14053937

[3]https://blog.csdn.net/lizuobin2/article/details/51785812

[4]杨兴强,刘翔鹏,刘毅.Linux进程状态演化过程的图形学表示[J].系统仿真学报,2013,25(10):2444-2448.

[5](荷)安德鲁 S。塔嫩鲍姆(Andrew S. Tanenbaum),(荷)赫伯特.博斯(Herbert Bos)著:陈向群等译,现代操作系统(原书第四版),机械工业出版社,2017

[6]张永选,姚远耀.Linux2.6内核O(1)调度算法剖析[J].韶关学院学报,2009,30(06):5-9.

[7]https://www.cnblogs.com/tianguiyu/articles/6091378.html

[8]http://www.360doc.com/content/15/0922/01/12144668_500602693.shtml

原文地址:https://www.cnblogs.com/yinbocheng/p/BUPTer.html

时间: 2024-10-31 16:41:47

关于linux操作系统中进程相关问题的学习笔记的相关文章

Linux操作系统中FastDFS部署手册

关于Linux操作系统中FastDFS部署手册 1.       部署环境 关于本次FastDFS_3.11版本的部署实验环境如下表所示: FastDFS部署环境 类别 OS FastDFS 机器台数 备注 版本 RedHat_Linux.5.4-x86_64 FastDFS_v3.11 2 表格 1 2.       组件和包安装 安装开发工具和所需的开发包 配置yum升级服务或安装系统时选择安装相关的依赖组件,这里使用yum对包进行安装,如下所示: yum groupinstall-y "d

深入理解Linux操作系统守护进程的意义

Linux服务器在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户.提供这些服务的程序是由运行在后台的守护进程(daemons)来执行的.守护进程是生存期长的一种进程.它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件.他们常常在系统引导装入时启动,在系统关闭时终止.linux系统有很多守护进程,大多数服务器都是用守护进程实现的.同时,守护进程完成许多系统任务,比如,作业规划进程crond.打印进程lqd等.有些书籍和资料也把

Linux操作系统中的系统调用接口函数

在分析病毒样本时发现有些系统函数是必用,有些超常用,现在都列出来,希望和大家交流 转载请注明出处:http://blog.csdn.net/u010484477     O(∩_∩)O谢谢 进程控制 fork 创建一个新进程 clone 按指定条件创建子进程 execve 运行可执行文件 exit 中止进程 _exit 立即中止当前进程 getdtablesize 进程所能打开的最大文件数 getpgid 获取指定进程组标识号 setpgid 设置指定进程组标志号 getpgrp 获取当前进程组

(笔记)Linux内核中内存相关的操作函数

linux内核中内存相关的操作函数 1.kmalloc()/kfree() static __always_inline void *kmalloc(size_t size, gfp_t flags) 内核空间申请指定大小的内存区域,返回内核空间虚拟地址.在函数实现中,如果申请的内存空间较大的话,会从buddy系统申请若干内存页面,如果申请的内存空间大小较小的话,会从slab系统中申请内存空间.有关buddy和slab,请参见<linux内核之内存管理.doc> gfp_t flags 的选项

Linux操作系统中“rpm”命令的详细介绍

Linux中常见的软件包封装类型如下所述: RPM软件包:这种软件包的扩展名为 ".rpm" ,只能在使用RPM机制的Linux操作系统中安装,如RHEL.Fedora.centOS等.RPM软件包一般针对特定版本的操作系统量身定制,因此依赖性较强. 源代码安装包:这种软件包是程序员开发完成的原始代码,一般被制作成 ".tar.gz" 或 ".tar.bz2" 等格式的压缩包文件,因多数使用 tar 命令打包而成的,所以经常被称为 "t

针对Linux操作系统中root用户无法登陆的解决方案

在Linux操作系统中,当我们使用字符界面操作时,往往不经意间会遇到root用户无法正常登陆的情况.一般对于初学者来说,看到这种情况发生,简直就是要了命了,很多人在这时就开始慌了神,怎么办?重装系统吧! Linux真特么不好用,好麻烦!哈哈~当然不是啦,只是你还不熟悉罢了!今天我就告诉大家如何通过进入单用户模式来修改root用户的密码(不用重装系统哦!) 下面以CentOS系列操作系统为例: 第一步:Linux开机引导时,按一下键盘上的 e 就可以进入到grub菜单界面 第二步:当电脑上出现gr

Linux系统中进程的创建

1.Linux中的进程 进程是程序执行的一个实例,也是系统资源调度的最小单位.如果同一个程序被多个用户同时运行,那么这个程序就有多个相对独立的进程,与此同时他们又共享相同的执行代码,在Linux系统中进程的概念类似于任务或者线程(task & threads). 进程是一个程序运行时候的一个实例实际上说的是它就是一个可以充分描述程序以达到了其可以运行状态的的一个数据和代码集合.一个进程会被产生并会复制出自己的子代,类似细胞分裂一样.从系统的角度来看进程的任务实际上就是担当承载系统资源的单位,系统

《Linux内核分析》第六周学习笔记

<Linux内核分析>第六周学习笔记 进程的描述和创建 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 [学习视频时间:1小时 撰写博客时间:2小时] [学习内容:进程创建的过程.使用gdb跟踪分析内核处理函数sys_clone] 一.进程的描述 1.1 进程描述符task_struct数据结构(一) 1. 进程控制块PCB——task_struct 为了管理进程,内核

contiki-main.c 中的process系列函数学习笔记 &lt;contiki学习笔记之六&gt;

说明:本文依然依赖于 contiki/platform/native/contiki-main.c 文件. ------------------------------------------------------------------------------------------------------------------------------------- 根据上一个笔记里面添加的printf()语句的打印信息提示,hello world 打印是在执行了 1 autostart_