[toc]
概述
本章有关于进程的概念比较难以理解。比如fork函数返回两个值,通过 fork 和 exec 来完成一些操作。
了解什么是程序,什么是进程。
带 * 号的标题可以看下,别的都是简单概念,方便自己回顾的
这个博客第一个迭代,先把概念讲清楚
第二个迭代上代码,开始撸进程这些函数的调用
异常 exceptions
异常概念
异常,就是控制权,因为某些事件,由用户而转移到内核系统,最后再跳转回来。
返回时有三种可能
- 返回到当前指令
- 返回给当前指令的下一条指令
- 终止当前程序
异常处理
异常由软硬件结合处理,程序计数器(pc,指哪里运行哪里)的更改硬件来做。但是具体处理逻辑是操作系统内核软件来做的。
异常表 exception tables,里面有各式各样的异常类型,和这种类型的处理方式。异常表起始地址保存在一个特殊的cpu寄存器里面:异常表基址寄存器 exception table base register
异常类似用调用,但有些重要的不同之处
跳转的时候,处理器总是会把返回地址压栈。但是异常压的可能是当前的栈,也可能是下一个指令的栈
处理器还会把额外的处理器状态压栈,返回时,重新开始执行被中断的程序需要这些状态。比如x86会把当前EFLAGS寄存器压栈
如果控制从用户到内核,所有的项目都会被压入内核栈,而不是用户栈
异常处理运行在内核态,意味着对所有系统的资源有完全的访问权
异常类型
类别 | 原因 | 异步/同步 | 返回行为 |
---|---|---|---|
中断 | 来自I/O设备的信号 | 异步 | 总是返回下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
-
中断 interrupt
来自处理器外部的 IO设备信号导致,硬件终端不是任何一条专门指令造成的,所以是异步的。比如定时器给cpu某个引脚发了个高电压的信号,cpu收到了,就读取这个异常信号,然后调用中断处理。
处理完了返回下一个指令,好像啥也没发生一样 -
陷阱 trap 又叫系统调用
陷阱是故意的异常,最重要的用途是:在用户程序和内核之间,提供一个像过程一样的接口,叫做系统调用。从程序员来看,系统调用和普通函数调用没啥区别,但是实现区别很大。普通函数在用户态,限制函数可以执行的指令类型,而且只能访问和调用函数相同的栈。
内核态允许系统调用执行特权,访问定义在内核中的栈。
-
故障 faults
由错误引起,但是有可能恢复。 两种决策,要么就是恢复,要么就是 abort中止经典故障——缺页异常。程序引用一个虚拟地址,而该地址相对应的物理页面不在内存中,因此要从磁盘里面取出来。等取完了,就可以返回给引起故障的指令,再执行一下,就没故障了。
如果你用一个数除以0,会出现浮点异常 floating exception,这个时候会终止。
终止
不可恢复的致命错误。比如磁盘损坏。
进程
进程概念
上述讲的都是低层次的控制转移。进程是高层的控制转移。
定义:进程是一个正在运行的程序的实例
程序可以存在于很多不同的地方,比如.c文件里面,比如.o文件里面。
进程是正在运行的程序的实例。
进程产生两种假象:
- 独立逻辑控制流,独占使用cpu和寄存器
你在运行这个进程的时候,永远不用担心有其他程序修改你的寄存器。
甚至都不被告知,这个系统上还有其他运行的程序 - 私有的地址空间
虚拟内存机制保证,每个程序都有自己私有的代码、数据、堆栈,你永远看不到其他进程正在使用的内存。仿佛你在独占的使用整个内存系统。
总而言之,给你一种,你拥有所有内存和处理器独占访问的权限。
并发
大家对处理器跳转着处理不同进程都不陌生的。
上下文切换
- 当前寄存器的值都会被复制到存储期保存
- 然后执行下一个进程,把下一个进程的寄存器信息放入cpu
- 把地址空间,切换到下一个进程的地址空间
地址空间加上寄存器的这些信息,就叫做,上下文。
上下文切换,就是地址空间和寄存器的变化。
上下文切换是由内核来控制的。
内核不像是一个正在运行的独立进程,它始终在某些现有的进程上下文中运行。
判断并发
每个进程都是一个逻辑控制流。
判断两个进程是不是并发的,要看他们运行的时候是否重叠。或者说,我在运行的时候,有没有被你给打断过,如果有,咱俩就是并发的。
比如 A和B、 A和C 都是并发的,但是 B 和 C并不是并发,而是串行。
进程控制
系统级函数error处理
Linux系统级函数,如果出现错误,通常返回 -1,他们将会用一个名为 errno 的全局变量来指示原因。
所以必须永远检查系统函数的返回值。
获取进程ID
getpid 获取当前进程ID
getppid 获取父进程ID
进程状态 *
进程有三种状态:
运行。要么在cpu上执行,要么被上下文切换了,等待被执行且最终会被内核调度。
停止。执行被挂起 suspended,且不会被调度。比如收到 SIGSTOP、SIGTSTP等信号,进程被停止。保持停止知道它收到一个SIGCONT信号
-
终止。进程永远停止。
三种原因:1、收到终止信号 2、 从主程序返回 3、 调用exit函数
从主程序返回,比如c总有一个main函数,从main函数结束时,就会终止里面的进程。exit有入参,就是状态,正常时0,不是0就是异常。但exit没有return
创建进程 int fork(void)
父进程可以通过 fork 函数来创建子进程。fork没有任何参数。
fork既在父进程返回,也在子进程返回。被父进程调用一次,返回两次,返回0到子进程,返回子进程ID到父进程
父子进程拥有相同但是独立的地址空间
可以理解为,完全相同的地址和堆栈,但却不是同一份。fork返回之后,比如父子进程访问的地址空间一摸一样,这意味着,所有变量,全局变量,栈,代码,都一样。子进程获得父进程打开的文件描述的相同副本
子进程可以访问任何已经打开的文件,包括父进程拥有的标准输入和标准输出。唯一不同是,父子进程PID不同
调用一次,返回两次
x在fork前定义,所以fork之后,父子进程都能看到独属于自己的 x == 1
if (pid == 0)这个逻辑只有子进程能走到,所以子进程中 x == 2
下面的 --x 只有父进程能走到,因为前面子进程被 exit了,所以 父进程里 x == 0
- 你永远无法保证进程的执行顺序
注意,上面的打印顺序不是绝对的,并发中,你永远无法保证是父进程先执行,还是子进程先执行。
如果fork的时候,内核决定父进程先执行,那就是先显示 x == 0,后显示 x == 2
多次fork
使用进程图,来更好的理解,多次调用fork。
虽然没法了解执行顺序,但是可以理解逻辑顺序。