《编译、链接与装载》笔记 (1) 现代操作系统任务调度

发布于 2021-09-03  37 次阅读


背景

小时候奇怪过这样一个问题:虽然CPU直接读取硬盘慢,但是硬盘放到内存再放到三级缓存再放到寄存器岂不是更慢

学了一点计组之后理解了其中的一小部分

正文

曾经在知乎问过这样一个问题,为什么硬盘主控不直接继承在CPU里面,当时被骂惨了2333

其实现代的硬盘(DMA下),是信号控制放行的,而且信号的处理不是由CPU本人接管,而是PCH模块代管,几乎不占用事实上的时间片(注意标题中“现代”二字,这里只考虑打开所有优化,且设备支持的情况,其他情况太多了,本文不与统计)

而且要理解带宽和延迟

这两个指标都可以用时间表示

我们从硬盘放到寄存器的操作,提高了事实上的瞬时延迟,但是提高了整体的交互带宽

那么,讲这么多和任务调度有啥关系?

well,在自动调整优先级的状态下,操作系统的时间片分配会倾向于自愿放弃剩余时间片或是频繁进入不占用CPU的IO的进程

在某个低优先级进程处于长期starvation时,CPU会自动逐渐提高其优先级,最终任何进程都能在等待足够长的时间后正常运行

另外,waitForSingleObject不是类似于PHP当中简单的sleep等待函数,在wait的时候,实际上会放弃所有时间片

所以一种良好的优化习惯是大量IO配合 waitForSingleObject 放弃时间片,提高后续CPU操作优先级

非抢占式的调度机制有一个非常巨大的好处,线程安全有高质量保障,但是不能保证每个程序员都能写出高质量的时间分配逻辑:这有可能导致其他程序永远卡死,无法获取时间片

因此现代的Windows操作系统基本都是抢占式的

回到Linux上

Linux其实并不区分线程和进程,只有task,每个task都是单线程的进程,具有独占的内存空间,执行文件实体和资源等,在实际意义上,共享统一内存空间的task为同一进程的不同线程

所以Linux的多线程如何实现?

这里需要涉及三个函数

fork() exec() clone()

fork会使用类似链表的方法控制父子关系

新进程的pid相对于父人物是0(可以理解为链表变成两层深度的关系)

该函数返回值永远为为pid,父进程返回的是真正的pid,子进程返回子pid

二者在top中的真实pid不同,相差1

fork时生成的内存空间会被标记为COW,读取时只生成一份,做到了时间复杂和空间复杂上的高效

exec会直接覆盖当前任务,包括上下文,全部堆栈,全部资源等,并继承原先的pid,除此之外exec函数不返回

一个常见的操作是:先fork出一片新空间,然后时候实际需要的任务代替这片空间(使用exec)

至于clone,他和fork类似,但是实际上创建的是线程,并从指定位置开始执行

fork创建的是相当于进程的任务