1.进程
驻留在内存中就绪等待运行的进程保存在就绪队列上,用链表的形式存储其PCB(进程控制块)。等待I/O设备的进程(会被中断)列表称为设备队列(I/O等待队列),每个设备都有自己的设备队列。
长期调度程序(作业调度程序)从磁盘中选择进程,短期调度程序(CPU调度程序)从就绪队列中选择进程。(还有中期调度程序,负责实现交换。Unix没有长期调度程序?)
进程可以区分为I/O-bound和CPU-bound(结合GPU cuda程序设计时遇到的问题)。长期调度程序应该平衡这两种进程的组合,否则会使得两个队列其中一个长时间为空。
上下文切换:保存原来进程的状态,装入新进程的保存状态,借助PCB完成。
fork创建子进程,子进程复制父进程的地址空间,两个进程都继续执行位于系统调用fork之后的指令,调用fork的返回值对于子进程来说是0,对于父进程来说是子进程的pid。execlp会从PATH环境中查找文件并传入参数执行。wait可以让子进程的资源彻底释放,wait(null)表示不关心子进程的终止状态,wait(&status)用status去接收子进程的终止状态表示你关心这点。
僵尸进程:如果父进程提早结束,子进程会以init作为其父进程,init会负责给它收尸;但如果子进程提前结束而父进程没有wait子进程,那子进程会在父进程生存期间变成僵尸进程(必经阶段,状态为“Z”),多种解决方法。
生产者-消费者模型:最基本的是靠自定义缓冲区实现数据交互。除此之外,操作系统提供了一些标准的进程间通信(IPC)机制,由消息传递系统提供。
消息传递系统:允许进程互相通信而不需要利用共享数据。包括直接通信(直接发向指定进程)、间接通信(通过邮箱或端口)。另外还需要考虑同步问题,即使阻塞还是非阻塞;还有缓冲队列的容量问题。(具体参照《Unix网络编程第二卷》)
RPC(远程过程调用):一个进程(或线程)调用一个远程应用的方法。RMI(远程方法调用):是java中类似RPC的一种功能,不局限不同的主机,可以是不同的java虚拟机。二者区别:1.RPC只能调用远程的子程序或函数,而RMI是基于对象的,支持调用远程对象的方法;2.RPC中远程调用的普通的数据结构,而RMI可以将对象作为参数传递给远程方法。
2.线程
线程(Lightweight Process)与同属一个进程的替他线程共享代码段、数据段和其他资源,但每个线程都有属于自己的线程ID、程序计数器、寄存器和堆栈。线程具有响应度高,资源共享,经济和充分利用多处理器体系结构的特点。
线程分为用户线程(如POSIX的pthread,由用户管理,容易引起阻塞)和内核线程(由内核管理,os调度的对象)。有多对一,一对一和,多对多模型(指用户线程对于内核线程)
线程取消包括同步取消和异步取消。
信号用来在线程进程间传递信息,有同步信号(非法内存访问,被零所除等)和异步信号(比如ctrl+c)。每个信号都有缺省信号处理程序(内核提供),也可以有用户定义的信号处理程序。信号可以发给进程内的某个线程、多个线程或所有线程。
用线程池来控制当前可用线程的数目。
POSIX标准的pthread创建示例:
pthread_t pid;
pthread_attr_t attr; //声明线程属性
pthread_attr_init(&attr);//初始化属性
pthread_create(&tid, &attr runner, argv[1]);//runner是该线程执行的函数
pthread_join(tid, NULL);//主函数等待线程完成(线程函数runner要调用pthread_exit(0))
linux可以用clone()来创建一个线程,通过传递标记来指出父进程有多少内容被子进程共享了。如果没有设置标记,那么clone和fork的动作一样。从本质上来说,Linux不区分进程和线程,可以使用pthread标准来实现用户级多线程编程(书中的这些说法太老了已经不太准确了,所说的linux不支持内核子程序是早期的linux版本,现在主流是1对1,ref这里)
3. CPU调度
抢占式(调度时间发生在运行状态->等待状态,或进程终止。抢占点必须安全,即内核数据结构不是在被修改时)和非抢占式(调度时间发生在运行->就绪,或等待->就绪)
调度算法:FCFS(非抢占式)、最短作业优先调度算法(SJF,可以是抢占式/非抢占式,提供最佳的平均等待时间。只能用于长期调度而不能在短期的层面上实现,不过可以使用加权指数平均来预测下一个CPU区间)、最短剩余时间优先算法(抢占式)、优先权调度(抢占/非抢占,会造成无穷阻塞问题,可以对低优先权进程进行“老化”,IBM 7094 6年啊...)、轮转调度算法(round-robin,抢占式,一般用于交互式业务。就绪队列作为循环队列,按照时间片对其轮询,增加了进程的切换。性能依赖于时间片大小,时间片要长于上下文切换的时间,但又不能太大,否则会变成FCFS,一般来说时间片应该大于80%的CPU区间)、多级队列调度(将就绪队列分成多个独立队列,队列之间可以有优先级,每个队列可以有自己的算法和时间片)、多级反馈队列调度(允许进程在队列间移动,长进程会自动沉到优先级低的队列,最复杂也最通用的算法)。
多处理器调度:进行负载分配,可以使用共同的就绪队列。多处理器架构包括对称多处理器和非对称多处理器。(在linux下,每个CPU都有着对应的可执行队列,而一个可执行状态的进程在同一时刻只能处于一个可执行队列中。)
实时调度:硬实时(刚性的,资源预约,运行在专门的硬件上和软件上,通常缺乏操作系统的全部功能但具有针对性,如卫星发射)、软实时(统计意义上的实时。保证实施进程必须有高的优先权且不被“老化”;保证分派延迟必须小,抢占点的定位、优先权倒置)。
算法评估:参数:CPU使用率,响应时间,吞吐量 |方法:确定性建模,排队模型,模拟,实现。
进程调度模型:线程分为用户级和内核级(有时还有LWP这一说,后面介绍),用户级线程由线程库管理,内核并不知道它们,在CPU上运行时,用户级线程最终要映射到一个相关的内核线程上,这种映射关系可能是直接的也可以是间接的(借助LWP)。用户级和内核级线程一个比较大的不同是它们的调度方式:1)进程本地调度,线程库调度用户线程在可用的LWP上运行,在应用程序内部进行;2)系统全局调度:决定调度哪个内核线程到CPU,内核级线程是系统调度的唯一单位。
- solaris 2采用基于优先级的进程调度。依次为实时、系统、分时和交互式。优先级和时间片大小呈反比关系;
- linux提供两种独立的调度算法(可参考这里和这里):一是分时算法,针对普通进程(基于静态优先级的信用度(动态优先级)的算法,每中断一次就失去一个信用直至重新计算,最终信用系统会赋予交互式进程和I/O约束进程--因为这些进程经常悬挂而不是中断--高的优先级),另一个是为实时任务设计的(包括FCFS和RR)。另外linux内核下运行的进程不可被抢占,用户模式下的进程才可以被抢占!(linux 2.6则实现了内核抢占,但是在很多地方还是为了保护临界区资源而需要临时性的禁用内核抢占。)
- linux如何确定进程优先级?
- - 在linux 2.4时,可执行状态的进程被挂在一个链表中。每次调度,调度程序需要扫描整个链表,以找出最优的那个进程来运行。复杂度为O(n);
- - 在linux 2.6早期,可执行状态的进程被挂在N(N=140)个链表中,每一个链表代表一个优先级,系统中支持多少个优先级就有多少个链表。每次调度,调度程序只需要从第一个不为空的链表中取出位于链表头的进程即可。这样就大大提高了调度程序的效率,复杂度为O(1);
- - 在linux 2.6近期的版本中,可执行状态的进程按照优先级顺序被挂在一个红黑树(可以想象成平衡二叉树)中。每次调度,调度程序需要从树中找出优先级最高的进程。复杂度为O(logN)。
LWP:light-weight-process,轻量级进程(参考wiki+《操作系统概念》p107)。LWP通常包括不同的概念:
1)在Unix System V和Solaris 2系统中,LWP运行在内核线程和用户线程之间,为同一个进程提供共享的地址和系统资源(一个进程可以有多个LWP,他们的地址都共享,一个线程库中的多个线程可以放在一个或多个LWP之上),向下每个LWP只能和一个内核级线程相连。LWP可以随时被创建和删除,线程只有需要和内核通信时才需要LWP。用户级线程通过线程库无须内核干扰的就可在LWP上实现调度和上下文切换,效率很高(因为无须system call,不过实施起来比较难)。
- 用户线程包括线程ID、寄存器集合、堆栈、优先权等,这些都放在用户空间;
- LWP包含它所执行的用户级线程的寄存器集合,还有内存和记账信息。LWP是内核的数据结构,驻留在内核上;
- 内核线程只有少量的数据结构(包括内存寄存器的拷贝,所关联的LWP指针)和一个堆栈,驻留在内核上;
- - 每个Solaris进程将信息存储在PCB中,包括:进程ID、内存图、打开文件列表、优先权、与该进程关联的内核线程类表的指针(LWP指针)。
2)其他操作系统中,没有单独的LWP一层,用户线程直接运行在内核线程之上。此时的LWP通常就单纯的指线程(thread),因为从概念上讲thread算是轻量级的process。
Ref: 《操作系统概念》