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

posted @ 2021-04-21 14:08  dynmi  阅读(66)  评论(0编辑  收藏  举报