1.串行程序与并发程序:
串行程序特指只能被顺序执行的指令列表.并发程序则是可以被并发执行的两个及以
上的串行程序的综合体.并发程序允许其中的串行程序运行在一个或多个可共享的
CPU上.同时也允许每个串行程序都运行在专为它服务的CPU上.前一种方式也称为多
元程序.由来自荷兰的图灵奖得主Edsger Wybe Dijkstra在1968年提出.它由操作
系统内核支持并提供多个串行程序复用多个CPU方法.多元处理是指计算机中多个
CPU共用一个存储器(即内存).并且在同一时刻可能会有数个串行程序分别运行在不
同的CPU之上.它由美国计算机科学家Anita K. Jones和Peter M. Schwar在1980
年提出.多元程序和多元处理是串行程序得以并发甚至并行运行的基础支撑.在现代计
算机系统中.它们已经得到了很好的融合.
2.并发程序与并行程序:
并发程序是指可以被同时发起执行的程序.而并行程序则被设计成可以在并行的硬件
上执行的并发程序.并发程序代表了所有可以实现并发行为的程序.是一个比较宽泛的
概念.其中包含了并行程序.
3.并发程序与并发系统:
并发程序属于程序.即使它被划分为许多部分(可以是规模更小的程序).只要这些部分
之间是紧密关联在一起.并且可以看作一个整体.那么它就属于一个程序.也可以称为一
个内聚软件单元.另一方面.程序与程序之间可以通过协商一致的协议进行通信.并且它
们之间是松耦合的.它们可以看作一个系统.而不是程序.并发程序和并发系统中的并发
的含义是一致的.但是.并发系统更有可能是并行的.因为其中的多个程序一般可以同时
在不同的硬件环境上运行.并发系统也常常称为并行系统.与并发系统同义的一个更加
流行的词是分布式系统.
4.并发程序的不确定性:
串行程序中所有代码的先后顺序都是固定的.而并发程序中只有部分代码是有序的.也
就是说.其中一些代码的执行顺序并没有明确指定.这一特性被称为不确定性.这导致并
发程序每次运行的代码执行路径都是不同的.即便是在输入数据相同的前提下.也是如
此.
5.并发程序内部的交互:
并发程序内部会被划分为多个部分.每个部分都可以看作一个串行程序.在这些串行程
序之间.可能会存在交互的需求.比如.多个串行程序可能都要对一个共享的资源进行访
问.又比如.它们需要相互传递一些数据.在这些情况下.就需要协调它们的执行.这就涉
及同步.同步的作用就是避免在并发访问共享资源时可能发生的冲突.以确保有条不紊
的传递数据.
感觉同步原则.程序如果想使用一个共享资源.就必须先请求该资源并获取到对它的访
问权.当程序不在需要某个资源的时候.它应该放弃对该资源的访问权(也称作释放资
源).一个程序对资源的请求不应该导致其他正在访问该资源的程序中断.而应该等到那
个程序释放资源之后再进行请求.在同一时刻.某个资源应该只被一个程序占用.
传递数据是并发程序内部的另一种交互方式.也称为并发程序内部的通信.协调这种内
部通信的方式不只"同步"这一种.也可以通过异步的方式对通信进行管理.这种方式使
得数据可以不加延迟的发送给数据接收方.即使数据接收方还没有为接收数据做好准
备.也不会造成数据发送方的等待.数据会临时存放在一个称为通信缓存的数据结构中.
通信缓存是一种特殊的共享资源.它可以同时被多个程序使用.数据接收方可以在准备
就绪之后按照数据存入通信缓存的顺序接收它们.
6.进程的定义:
进程是Unix及其衍生操作系统(包括linux操作系统)的根本.因为所有的代码都是在进
程中执行的.通常把一个程序的执行称为一个进程.反过来讲.进程用于描述程序的执行
过程.程序和进程是一对概念.它们分别描述了一个程序的静态形式和动态特征.除此之
外.进程还是操作系统进行资源分配的一个基本单位.
7.进程的衍生:
进程使用fork(一个系统调用函数)可以创建若干个新的进程.其中前者称为后者的父
进程.后者称为前者的子进程.每个子进程都是源自它父进程的一个副本.它会获得父进
程的数据段 堆和栈的副本.并与父进程共享代码段.每一份副本都是独立的.子进程对
属于它的副本的修改对其父进程和兄弟进程(同父进程)是不可见的.反之亦然.全盘复
制父进程的数据是一种相当低效的做法.Linux操作系统内核(以下简称内核)使用写时
复制(Copy on Write.简称COW)等技术来提高进程创建的效率.刚创建的子进程也
可以通过系统调用exec把一个新的程序加载到自己的内存中.而原先在其内存中的数
据段 堆 栈以及代码段就会被替换掉.在这之后.子进程执行的就会是那个刚刚加载进
来的新程序.
Unix/Linux操作系统中的每一个进程都有父进程.所有的进程共同组成了一个树状结
构.内核启动进程作为进程树的根.负责系统的初始化操作.它是所有进程的祖先.它的
父进程就是它自己.如果某一个进程先于它的子进程结束.那么这些子进程就会被内核
启动进程"收养".成为它的直接子进程.
8.进程的标识:
为了管理进程.内核必须对每个进程的属性和行为进行详细的记录.包括进程的优先级
状态 虚拟地址范围以及各种访问权限.等等.这些信息都会被记录在每个进程的描述符
中.进程描述符并不是一个简单的符号.而是一个复杂的数据结构.保存在进程描述符中
的进程ID(常称为PID)是进程在操作系统中的唯一标识.其中进程ID为1的进程就是之
前提到的内核启动进程.进程ID是一个非负整数且总是顺序的编号.新创建的进程ID总
是前一个进程ID递增的结果.此外进程ID也可以重复利用.当进程ID达到其最大限值
时.内核会从头开始查找闲置的进程ID并使用最先找到的那个作为新进程的ID.进程描
述符中还会包含当前进程的父进程的ID(常称为PPID).
func main() {
pid := os.Getpid()
ppid := os.Getppid()
fmt.Println(pid, ppid)
}
注:PID并不传达与进程相关的任何信息.它只是一个用来唯一标识进程的数字而已.进
程属性信息只包含在内核中与PID对应的进程描述符里.PPID体现了两个进程之间的
亲缘关系.
进程ID对内核以外的程序非常有用.内核可以高效的把进程ID转换成对应进程的描述
符.可以shell命令kill终止某个进程ID对应的进程.还可以通过进程ID找到对应的进程
并向它发送信号.
9.进程状态:
在Linux操作系统中.每个进程在每个时刻都是有状态的.可能状态共有六个.分别是可
运行状态 可中断的睡眠状态 暂停状态或跟踪状态 僵尸状态和退出状态.
1).可运行状态(TASK_RUNNING.简称为R):如果一个进程处于在该状态.那么说明
它立刻要或正在CPU上运行.不过运行的时机是不确定的.这由进程调度器决定.
2).可中断的睡眠状态(TASK_INTERRUPTIBLE.简称为S):当进程正在等待某个事件
(比如网络连接或信号量)到来时.会进入此状态.这样的进程会被放入对应事件的等待
队列中.当事件发生.对应的等待队列中的一个或多个进程就会被唤醒.
3).不可中断的睡眠状态(TASK_UNINTERRUPTIBLE.简称为D):此种状态与可中断
的睡眠状态的唯一区别就是它不可被打断.这意味着处在此种状态的进程不会对任何
信号做出响应.发送给此状态的进程的信号直到它从该状态转出才会被传递过去.处于
此状态的进程通常是在等待一个特殊的事件.比如等待同步的I/O操作(磁盘I/O等)完
成.
4).暂停状态或跟踪状态(TASK_STOPPED或TASK_TRACED.简称为T):向进程发送
SIGSTOP信号.就会使该进程转入暂停状态.除非该进程正处于不可中断的睡眠状态.
向正处于暂停状态的进程发送SIGCONT信号.会使该进程转向可运行状态.处于该状
态的进程会暂停.并等待另一个进程(跟踪它的那个进程)对它进行操作.例如.使用调试
工具GDB在某个程序中设置一个断点.而后对应的进程在运行到该断点处就会停下来.
这时.该进程处于跟踪状态.跟踪状态与暂停状态非常类似.向处于跟踪状态的进程发送
SIGCONT信号并不能使它恢复.只有当调试进程进行了相应的系统调用或者退出后,
它才能恢复.
5).僵尸状态(TASK_DEAD_EXIT_ZOMBIE.简称为Z):处于此状态的进程即将结束运
行.该进程占用的绝大多数资源也都已经被回收.不过还有一些信息未删除.比如退出码
以及一些统计信息.之所以保留这些信息.主要是考虑到该进程的父进程可能需要它们.
由于此时的进程主体已经被删除而只留下一个空壳.故此状态称为僵尸状态.
6).退出状态(TASK_DEAD_EXIT_DEAD.简称为X):在进程退出的过程中.有可能连退
出码和统计信息都不需要保留.造成这种情况的原因可能是显示的让该进程的父进程
忽略掉SIGCHLD信号(当一个进程消亡的时候.内核会给其父进程发送SIGCHLD信号
以告知此情况).也可能是该进程已经被分离(分离即让子进程和父进程分别独立运行).
分离后的子程序将不会在使用和执行与父进程共享的代码段指令.而是加载并运行一
个全新的程序.在这些情况下.该进程在退出的时候就不会转入僵尸状态.而会直接转入
退出状态.处于退出状态的进程会立即被干净利落的结束掉.它占用的系统资源也会被
操作系统自动回收.
流程图:
10.进程的空间:
用户进程(或者说程序的执行示例)总会生成在用户空间中.它可以做很多事.但是却不
能与其所在的计算机硬件进行交互.内核可以与硬件交互.但是它却生存在内核空间中.
用户进程无法直接访问内核空间.用户空间和内核空间都是操作系统在内存上划分出
一个范围.它们共同瓜分了操作系统能够支配的内存区域.体现了Linux操作系统对物
理内存的划分.
内存区域中的每一个单元都是有地址的.这些地址由指针来标识和定位.通过指针来寻
找内存单元的操作也称为内存寻址.指针是一个正整数.由若干个二进制位表示.具体的
二进制位的数量由计算机(更确切的说是CPU)的字长决定.因此.在32位计算机中可以
有效标识2的32次方个内存单元.而在64位计算机中可以有效表示2的64次方个内存
单元.
这里说的地址并非物理内存中的真实地址.而是虚拟地址.由虚拟地址来标识的内存区
域又称为虚拟地址空间.有时也称为虚拟内存.
注:虚拟内存的最大容量与实际可用的物理内存大小无关.内核和CPU会负责维护虚拟
内存与物理内存之间的映射关系.
内核会为每个用户进程分配的是虚拟内存而不是物理内存.每个用户进程分配到的虚
拟内存总是在用户空间中.而内核空间则留给内核专用.每个用户进程都认为分配给它
的虚拟内存就是整个用户空间.一个用户进程不可能操纵另一个用户进程的虚拟内存.
因为后者的虚拟内存对于前者来说是不可见的.换句话说.这些进程的虚拟内存几乎是
彼此独立的 互不干扰的.这是由于她们基本上被映射到了不同的物理内存之上.
内核会把进程的虚拟内存划分为若干页.而物理内存单元的划分由CPU负责.一个物理
内存单元被称为一个页框.不同进程的大多数页都会与不同的页框对应.
11.系统调用:
用户进程生存在用户空间中且无法直接操作计算机硬件.但是内核空间中的内核却可
以做到.用户进程无法直接访问内核空间.也无法随意指使内核去做它能做的一些时.为
了使用户进程能够使用操作系统更加底层的功能.内核会暴露出一些接口供他们使用.
这些接口是用户进程使用内核功能(包括操作计算机硬件)的唯一手段.也是用户空间
和内核空间的一座桥梁.用户进程使用这些接口的行为称为系统调用.不过在很多时
候"系统调用"这个词也指内核提供的这些接口.注意.虽然系统调用也是由函数呈现.但
它与普通函数有明显区别.系统调用是向内核空间发出一个明确的请求.而普通函数只
是定义了如何获取一个给定的服务.更重要的是系统调用会导致内核空间中数据的存
取和指令的执行.而普通函数却只能在用户空间中有所作为.如果在一个函数的函数体
中包含了系统调用.那么它执行也将涉及对内核空间的访问.但是这种访问仍然是通过
函数体内的系统调用来完成的.系统调用是内核的一部分.普通函数却不是.
说到系统调用.就不得不提及另外一对概念----内核态和用户态.为了保证操作系统的
稳定和安全.内核依据由CPU提供的 可以让进程驻留的特权级别建立了两个特权状
态---内核态和用户态.大部分时间里.CPU都处于用户态.这时CPU只能对用户空间进
行访问.CPU在用户态下运行的用户进程是不能与内核接触的.当用户进程发出一个系
统调用的时候.内核会把CPU从用户态切换到内核态.而后会让CPU执行对应的内核函
数.CPU在内核状态下是有权限访问内核空间的.这就相当于用户进程可以通过系统调
用使用内核提供的功能.当内核函数执行完毕后.内核会把CPU从内核态切换回用户态.
并把执行结果返回给用户进程.
12.进程的切换和调度:
与其他分时操作系统一样.Linux操作系统也可以凭借CPU的威力快速的再多个进程
之间切换.这也称为进程间的上下文切换.如此会产生多个进程同时运行的假象.而每个
进程都会认为自己独占了CPU.这就是多任务操作系统称谓的由来.不过.无论切换速度
如何.在同一时刻正在运行的进程仅会有一个.
切换CPU正在运行的进程是需要付出代价的.内核此刻要换下正在CPU上运行的进程
A.并让CPU开始运行进程B.再换下进程A之前.内核必须要及时保存进程A的运行时状
态.另一方面.假设进程B不是第一次运行.那么让进程B重新运行之前.内核必须保证已
经依据之前保存的相关信息把进程B恢复到之前被换下时的运行时状态.这种在进程换
出换入期间必须要做的任务称为进程切换.这个任务主要还是由内核完成.除了进程切
换.为了使各个生存着的进程都有运行的机会.内核还要考虑下次切换时运行哪个进程
何时进行切换 被换下的进程何时在换上.等等.解决类似问题的方案和任务统称为进程
调度.
语雀地址www.yuque.com/itbosunmian…?
《Go.》 密码:xbkk 欢迎大家访问.提意见.
悠悠的往事回流的水.