zoukankan      html  css  js  c++  java
  • LAB4 多进程调度

    我们思考这几个问题:

    • 操作系统kernel要如何实现多处理器支持?
    • 用户进程能不能由用户进程创建?怎么实现?提到fork()、exec()你想到什么?
    • 操作系统kernel要如何支持多进程?你设计的多进程调度可能在哪些地方会有性能弊端?如何去改进?
    • 你的kernel支持了多进程,那两个(多个)用户进程之间可不可以进行通信呢?要怎么设计呢?

    多处理器支持

    • JOS将多CPU分成一个BSP、多个AP。每个CPU对资源有等效访问权。BSP负责初始化系统、引导启动操作系统并激活应用程序处理器APs。开机时和开机后的准备工作由BSP处理,在准备工作中BSP根据MP表逐个激活AP,每个AP在内存上都有一段自己的栈。
    • 假如多个CPU同时访问同一个I/O设备怎么办?假如多个CPU同时写同一段内存怎么办?假如多个CPU同时发生了中断要转入内核态怎么办?
      • 前两个问号:多个CPU对资源同等访问权,所以我们采用加锁的方式来应对这个情况。
      • 第三个问号:内核态下是全能的,也是要慎重的。假若多个CPU同时工作在内核态下,那管理起来很恶心的。最优雅的设计是设置一个内核锁,任意时刻只允许一个CPU处于内核态。

    用户进程能不能由用户进程创建?

    你设计的OS应该支持这个!

    • 我们可以去想,就两种情况:一个是User Environment复制出和自己一样的User Environment,一个是User Environment从外存load一个User Environment到操作系统进程列表里。这就是大家熟知的fork()和exec()。
    • fork()的逻辑是这样的:在操作系统进程链表中添加新进程;复制原进程的内存段给fork出的新进程;将新进程加入进程调度队列。如果你去看linux,windows这些成品的设计和运行逻辑的话,你会发现fork()的使用频率很高。而fork操作复制内存段时的时间开销是很拖沓让人不舒服的,那我们就想想怎么优化它?我们发现,很多情况下,A去fork出B后,B并不会急于修改自己的内存段。那可不可以fork时不copy内存空间,直接让新进程的内存空间指针指向原进程的?可以!只要当原进程或新进程希望修改自己内存段时把它们分开就好了。也就是变相延后或者省掉了fork的内存复制操作。这项设计叫“copy-on-write fork”。

    多进程支持

    从字面意义上看“多进程”,很好理解,把多个User Environment分配到多个CPU上就行了。但是动起手来会发现,这个“调度”的设计很硬核。如果设计不好,在用户视角上看到的是等待拖延宕机,这必然是整个操作系统最要命的点。。。

    调度算法focus on 这两点:

    • 调度进程上CPU的先后顺序
    • 被调度进程每次上CPU后要执行多久

    我们所面临的问题是这样的:

    • 每时每刻都有新进程加入,老进程退出。
    • 用户希望多个User Environment可以同时跑,但主板上的CPU核心数量有限,绝大多数情况下,要运行的User Environment数远大于处理器数。
    • 不同进程的时效性不同,对用户的重要性不同。

    JOS中只实现了一个简单的轮转调度算法。而目前Linux的多进程调度算法急于完全公平调度算法实现,完全公平调度(CFS)到最后一节看。

    img

    多用户进程通信

    为什么需要“多进程通信”?

    数据传输,资源共享,通知事件,进程同步、互斥。

    怎么去实现“多用户通信”?

    在内存上开一段空间,让多个进程都可以访问。想一下,如果这段内存区间是在某一个用户进程的地址空间内,那会很难管理吧?所以这段内存要开在内核区上。

    至此,我们可以明确:“多用户通信”就是在内核区内存开一段缓冲区,多个用户进程按照一定的规则分别对这段缓冲区有一定的操作权限。

    “规则”怎么讲?我们看看Linux都有什么“规则”:

    “pipe” 无名管道

    只用于具有亲缘关系的进程间通信(父子进程、兄弟进程)。

    严格A进程写,B进程读的规则。数据只能单向流动。半双工。

    多通过read(),write()进行操作。

    其原型为

    #include <unistd.h>
    int pipe(int fd[2]); //成功返回1,失败返回-1
    

    其中fd[1]是用来写的文件描述符,fd[0]是用来读的文件描述符。

    FIFO 命名管道

    以特殊设备文件的形式存在于外存中。类似于在进程间使用文件来传输数据。

    数据读出时,FIFO管道中同时清除数据。

    用read(),write()进行操作。

    其原型为:

    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode); // 成功返回0,失败返回-1
    

    共享内存

    需要使用信号量对多个要访问它的进程进行同步。
    其原型为:

    #include <sys/shm.h>
    
    // 创建或获取一块共享内存:成功返回其ID,失败返回-1
    int shmget(key_t key, size_t size, int flag);
    
    // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
    void *shmat(int shm_id, const void *addr, int flag);
    
    // 断开与共享内存的连接:成功返回0,失败返回-1
    int shmdt(void *addr);
    
    // 控制共享内存的相关信息:成功返回0,失败返回-1
    int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
    

    socket网络通信

    只用于通过网络通信的在两台机器上的两个用户进程。

    完全公平调度算法(CFS)

    是在linux 2.6.23(2007年10月)中出现的调度算法。由Ingo Molnár所提出。

    对于每个CPU核心,归属于它的所有RUNNING的进程用一个队列进行维护,即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运行队列中  
        ...
    };
    

    数据结构中的每个进程,都有一个属性:虚拟运行时间(vruntime)。它在进程的sched_entity结构体里。

    vruntime = 实际运行时间 * 1024 / 进程权重
    
    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; //进程此时在哪个运行队列中
    };
    

    每个cfs_rq内所有进程的sched_entity使用红黑树维护。vruntime作为键值。

    每次时钟中断发生时;更新当前进程的vruntime;在红黑树中找到vruntime最小的进程抢占当前进程,当然如果还是它自己就无抢占一说。时间复杂度O(logN)。

    新进程加入时:初始化其vruntime,并将其插入到红黑树中。时间复杂度O(logN)。

    旧进程销毁时:从红黑树中删除该节点。时间复杂度O(logN)。

    新进程的vruntime初始值怎么安排?

    每个CPU的运行队列cfs_rq都维护一个min_vruntime变量。新进程的初始vruntime值以它所在运行队列的min_vruntime为基础来设置,这样就能与老进程保持在合理的差距范围内。

    休眠进程vruntime值一直不变吗?

    休眠进程被唤醒时重设vruntime,以min_vruntime为基础给予一定补偿。这样能把进程间的vruntime保持在合理的差距范围内。

    进程从CPU A切换到CPU B上,vruntime怎么变化?

    新的vruntime = 旧的vruntime - min_vruntime_A + min_vruntime_B

  • 相关阅读:
    SQL Server索引进阶:第十二级,创建,修改,删除
    SQL Server索引进阶第十一篇:索引碎片分析与解决
    Object.create()和new object()和{}的区别
    vue 前后端分离nginx部署
    实现组件props双向绑定解决方案
    prop不同数据类型设置默认值
    vue + element ui 阻止表单输入框回车刷新页面
    Vue.js中 watch(深度监听)的最易懂的解释
    vue-resource和axios区别
    JS中 reduce() 的用法
  • 原文地址:https://www.cnblogs.com/dynmi/p/14684992.html
Copyright © 2011-2022 走看看