1.用户空间和内核空间
操作系统为了支持多个应用同时运行,需要保证不同进程之间相对独立,这样的话,一个进程的崩溃不会影响其他的进程,恶意进程不能直接读取和修改其他运行时的代码和数据。因此操作系统内核需要拥有高于普通进程的权限,以此来调度和管理用户的应用程序。
于是,内存空间被划分为两部分,一部分是内核空间,一部分是用户空间。内核空间存储的代码和数据具有更高级别的权限。内存访问的相关硬件在程序执行期间会进行访问控制(Access Control),使得用户空间的程序不能直接读写内核空间的内存。
2.文件描述符
File Description,也称fd,我们最熟悉的句柄为0、1、2这三个。0表示标准输入;1表示标准输出;2表示标准错误输出。
0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。
文件描述符的操作demo如下所示:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv)
{
char buf[10] = "";
read(0, buf, 9); /* 从标准输入 0 读入字符 */
// fprintf(stdout, "%s\n", buf); /* 向标准输出 stdout 写字符 */
write(1, buf, strlen(buf));
return 0;
}
3.进程切换
进程的执行依赖CPU分配的时间片,不同线程可以在同一个CPU上执行。对同一个进程来说,它所获取的时间片是不连续的,而针对CPU来说,他分配的时间片是连续的,这里就有一个进程切换的工作需要完成。
进程切换过程如下图所示:
上图展示了进城切换中几个最重要的步骤:
(1)当一个程序正在执行的过程中,中断(interrupt)或系统调用(system-call)发生可以使得CPU的控制权从当前进程转移到操作系统内核。
(2)操作系统内核负责保存进程i在CPU的上下文(程序计数器,寄存器等)到PCBi(操作系统分配给进程的一个内存块)中。
(3)从PCBj取出进程j的CPU上下文,将CPU控制权转移给进程j,开始执行进程j的指令。
从图中可以看出,操作系统在进行进程切换时,需要进行一系列的内存读写操作,这带来了一定的开销。
4.进程阻塞
我们所说的“阻塞”是指进程在发起了一个系统调用(system call)后,由于该系统调用的操作不能立即完成,需要等待一段时间,于是内核将进程挂起为等待(waiting)状态,以确保他不会被调度执行,占用CPU资源。
4.1.进程状态转换
进程状态转换逻辑如下图所示:
可以看到,进程共有五种不同的状态:
(1)new:进程正在被创建。
(2)running:进程的指令正在被执行。
(3)waiting:进程正在等待一些事件的发生(例如I/O的完成或者收到某个信号)。
(4)ready:进程在等待被操作系统调度。
(5)terminated:进程执行完毕,也可能是被强制终止的。
4.2.图示阻塞原理
下面通过图片的演示来展示阻塞的过程。
假如工作队列中有三个进程正在执行,如下图所示:
这个时候,三个进程都是处于“RUNNING”状态。
对于socket来说,当发生阻塞的时候,调用阻塞程序,而阻塞程序最重要的一个操作就是将进程从工作队列移除,并且将其加到等待队列。这就完成了进程从RUNNING状态转变为WAITING状态。如下图所示:
当发生中断时,调用中断程序最重要的一个操作就是将等待队列中的进程重新移回工作队列,继续分配系统的CPU资源。
这个时候,进程A从WAITING状态转变为READY状态了,可以等待操作系统调度运行。
5.同步与异步
同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。也就是说调用会等待返回结果计算完成才能继续执行。
异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。也就是说,其实异步调用会直接返回,但是这个结果不是计算的结果,当结果计算出来之后,才通知被调用的程序。
同步和异步都是用于表示进程间关系的。同步的任务进程间是有依赖的,异步的任务进程间是相互独立的。
6.阻塞与非阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务。
非阻塞调用是不管可不可以读写,它都会返回,返回成功说明读写操作完成了,返回失败会设置相应errno状态码,根据这个errno可以进一步执行其他处理。它不会像阻塞IO那样卡住不动。
阻塞与非阻塞是用于表示单一任务进程内的一种状态。