961全部内容链接
文章目录
- 编译系统的过程
- 静态链接
- 目标文件
- 符号和符号表
- 重定位和加载
- 动态链接库
- 异常和进程
-
- 进程控制和信号
-
- 进程间的通信
- 进程间信号量的控制
- 信号量
- 各种并发编程模式
- 共享变量和线程同步
- 其他并行(发)问题
编译系统的过程
《CSAPP》P3
一个C程序编译分为以下几个阶段:
- 预处理阶段:预处理器(cpp,不是c++的那个cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中的第一行#include 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常以 .i 作为文件扩展名
- 编译阶段:编译器(ccl)将文本文件hello.i 翻译成文本文件hello.s,它包含了一个汇编语言程序(就是里面是汇编代码)。通俗的说就是把C代码转成汇编代码
- 汇编阶段:汇编器(as,编译汇编的编译器)将hello.s 文件编译成可重定位目标程序,hello.o。 该文件是二进制文件。其实到这已经可以执行了,只不过可能会报错,因为这里面调用了printf函数,所以相同目录下必须要有printf.o文件才能正常执行。有点类似java的程序,在其目录下必须有jar包一样,要不然会报ClassNotFound
- 链接程序:因为hello代码调用了printf函数,所以要把printf.o文件合并到hello.o文件中,链接器(ld)就负责做这件事,最终会得到一个hello文件,该文件是一个可执行文件。
静态链接
《CSAPP》 P464
链接(linking) 是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
静态链接器(比如Linux LD程序)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。链接器主要完成两个任务:
- 符号解析(symbol resolution):目的是将每个符号引用正好和一个符号定义关联起来。
- 重定位(relocation):编译器生成的目标文件的地址都是从0开始的。要把他们链接起来,总要有个先后顺序,要不然到运行的时候,访问0地址谁知道是哪个目标文件的0地址。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们执行这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
目标文件
《CSAPP》 466
目标文件有三种形式:
- 可重定位目标文件:包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件结合起来,创建一个可执行目标文件。
- 可执行目标文件:包含二进制代码和数据,可以被直接复制到内存并执行。
- 共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或运行时被动态地加载进内存并链接。
符号和符号表
《CSAPP》P468
每个可重定位目标模块(就是目标文件)m都有一个符号表,它包含m定义和应用的符号信息。在链接器的上下文中,有三种不同的符号:
- 全局符号:模块m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数和全局变量。
- 外部符号:由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号,对应于在其他模块中定义的非静态C函数和全局变量。
- 局部符号:只被模块m定义和引用的局部符号。他们对应带static属性的C函数和全局变量。这些符号在模块m中任何位置都可见,但是不能被其他模块引用。
符号解析的概念:链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。
链接器链接时,若多个输入的模块中,存在重名的全局符号。链接器的做法为:将已经初始化的全局变量定义为强符号,未初始化的全局变量定义为弱符号。然后根据以下规则来选择:
- 不允许有多个重名强符号
- 如果有一个强符号和多个弱符号重名,则选择强符号
- 如果有多个弱符号重名,则任意选择一个弱符号。
重定位和加载
《CSAPP》P478
一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。现在就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。
加载可执行目标文件:
要运行可以执行目标文件prog,在Linux Shell的命令行中输入它的名字即可运行:
linux> ./prog
因为prog不是一个内置的shell命令,所以shell会认为prog是一个可执行目标文件,然后会调用加载器(loader) 来运行它。Linux中,是通过调用execve函数来调用加载器的。加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一个指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载。
加载器实际是如何工作的:Linux系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当Shell运行一个程序时,父shell进程生成一个子进程,它是父进程的一个复制。子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,此时,操作系统利用他的页面调度机制自动将页面从磁盘传送到内存。
动态链接库
《CSAPP》P485 7.10动态链接共享库
静态库的缺点:
- 静态库需要显式地将他们的程序与更新了的库重新链接
- 几乎每个C程序都是用标准I/O函数,如printf。若使用静态库,则每个程序都要各自单独加载printf,并为其分配内存,造成了极大地资源浪费。
共享库(shared library) 可以解决静态库的问题。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接(dynamic linking),该过程由动态链接器(dynamic linker) 完成。 共享库也称为共享目标(shared object),在linux系统中,通常以.so后缀,在windows中,以.dll为后缀。
异常和进程
异常的相关概念
《王道2021操作系统》P17 中断和异常的概念
CPU的状态分为内核态和用户态:
- 内核态(核心态):操作系统内核工作在内核态
- 用户态:用户程序工作在用户态
系统不允许用户程序实现内核态的功能,而它们又必须使用这些功能。因此,需要在内核态建立一些“门”,以便实现从用户态进入内核态。在实际操作系统中,CPU运行上层程序时唯一进入这些“门”的途径就是通过中断或异常。当发生中断或异常时,运行用户态的CPU会立即进入内核态,这是通过硬件实现的。
中断(Interruption)的定义:中断也称为外中断,指来自CPU执行指令以外的事件的发生,如设备发出的I/O结束中断,告诉CPU设备I/O操作已经完成,可以进行下一步操作了。时钟中断,表示一个固定的时间片已到,让处理机处理计时、启动定时运行的任务等。
异常(Exception)的定义 异常也称为内中断、例外或陷入(trap),指源自CPU执行指令内部的事件,如程序的非法操作码、地址越界、算术溢出、虚拟系统的缺页及专门的陷入指令等引起的事件。
《CSAPP》P504
异常的类别:
类别 |
原因 |
异步/同步 |
返回行为 |
解释 |
中断(interrupt) |
来自I/O设备的信号 |
异步 |
总是返回到下一条指令 |
|
陷阱(trap) |
有意的异常 |
同步 |
总是返回到下一条指令 |
陷阱的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用 |
故障(fault) |
潜在可恢复的错误 |
同步 |
可能返回到当前指令 |
故障由错误引起。当故障发生时,处理器将控制转移给故障处理程序。若能够修正,则返回到引起故障的指令,从而重新执行它。若不能修复,则会引发终止 |
终止(abort) |
不可恢复的错误 |
同步 |
不会返回 |
终止时不可恢复的致命错误造成的结果,通常是一些硬件错误 |
进程的相关概念
《CSAPP》P508
进程就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
《王道2021操作系统》 P31
PCB的概念:为了使参与并发执行的程序(含数据)能独立运行,必须为之配置一个专门的数据结构,称为进程控制块(Process Control Block,PCB)。系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程。
进程的七状态模型:
- 创建态:进程正在被创建
- 就绪态:进程获得了除CPU外的一切所需资源,一旦得到CPU,便可立即运行。系统中处于就绪状态的进程有多个,通常将他们排列成一个队列,称为就绪队列。
- 阻塞态:进程正在等待某一事件发生而暂停运行,如等待某资源可用(不包括CPU)或等待IO完成等。此时即使处理机空闲,该进程也不能运行。
- 运行态:进程正在CPU上运行,如果计算机只有一个CPU,那么同一时刻,只能有一个进程处于运行态。
- 挂起就绪:处于就绪状态的进程,由于内存不足,被暂时换出到了外存。
- 挂起阻塞:处于阻塞状态的进程,由于内存不足,被暂时换出到了外存。
- 结束态:进程正在从系统消失,可能是进程正常结束或其他原因中断退出运行。进程需要结束运行时,系统首先必须置该进程为结束态,然后再进一步处理资源释放和回收等工作。
进程状态之间的转换:
- 新建->就绪:系统为进程申请PCB,内存等资源。申请完毕后,就会进入就绪状态,若内存不足,则会进入挂起就绪状态。
- 挂起就绪-就绪:若内存不足,操作系统会暂时把内存中不运行的进程从内存换出到外存。等到内存充足时,再换入到内存
- 挂起阻塞-阻塞:与“挂起就绪-就绪”一致
- 就绪->运行:进程在就绪队列中排队,当轮到它时,就会获得CPU资源,于是进程就从就绪态变为了运行态
- 运行->就绪:处于运行态的进程在时间片用完后,就得让出CPU,给后面排队的进程用。若该系统为可抢占系统,则还没用完时间片的进程,就有可能被后面高优先级的进程抢占CPU。
- 运行->阻塞:若运行的程序等待某一资源发生时(比如你写了个scanf,等待键盘输入),此时该进程就会从运行态转为阻塞态,当该事件发生后,再从阻塞态变为就绪态。若此时进程是在外存当中,则会从挂起阻塞态变为挂起就绪态。
进程控制和信号
《王道2021操作系统》P32
进程控制的主要功能是对系统中的所有进程实施有效的管理。一般把进程控制用的程序段称为原语。原语的特点是执行期间不允许中断,是一个不可分割的基本单位。
1.进程的创建
允许一个进程(父进程)创建另一个进程(子进程)。子进程可以继承父进程所拥有的资源。当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程。此外,撤销父进程时,必须同时撤销其所有子进程。
操作系统创建一个进程的过程如下(创建原语):
- 为新进程分配一个唯一的进程标识符,并申请一个空白的PCB。若PCB申请失败,则创建失败
- 为进程分配资源,如内存资源等,若资源不足,则进程暂时处于阻塞态,等待内存资源。
- 初始化PCB
- 若进程就绪队列能够接纳新进程,则将新进程插入就绪队列,等待被调度运行。
2. 进程的终止
引起进程的终止主要有:
- 正常结束:进程正常运行完毕退出
- 异常结束:进程报错,导致进程终止。如指针越界,算术运算错,IO故障等
- 外界干预:外界请求,让进程终止运行。如 kill -9 ,父进程终止等。
进程终止的过程(撤销原语):
- 根据被终止进程的标识符,检索PCB,从中读出该进程的状态
- 若该进程处于执行状态,则立即终止该进程的执行,并让出CPU资源。
- 若该进程存在子进程,则将其所有子进程终止
- 将该进程所拥有的全部资源,归还给其父进程,若没有父进程,则归还给操作系统
信号
《CSAPP》P526
信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。信号提供了一种机制,通知用户进程发生了这些异常。比如,如果一个进程试图除以0,那么内核就给它发送一个SIGFPE信号。
传送一个信号到目的进程由两个不同的步骤组成:
- 发送信号:内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。一般有两种情况:①内核检测到了一个系统事件,如 ÷0 错误。②一个进程调用了kill函数,显式的要求内核发送一个信号给目的进程。比如经典的 kill -9 就是给目的进程发送一个信号,让它强行终止。
- 接受进程:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接受了信号。进程可以忽略这个信号,终止或者执行信号处理程序来捕获这个信号。
进程间的通信
《王道2021操作系统》 P35
进程通信是指进程之间的信息交换。主要有三种方式:
- 共享存储:在通信的进程之间存在一块可直接访问的共享空间(如前面提到的内存映射技术,mmap)。通过对这片共享空间进行读写操作实现进程之间的信息交换。
- 消息传递:进程间的数据交换以消息(Message)为单位。有两种通信方式:①直接通信方式,发送进程直接把消息发送给接受程序。②间接通信方式:把消息发送到消息中间件(MQ)上,接受进程从消息中间件取得消息
- 管道通信:管道指用于连接一个读进程和一个写进程以实现他们之间的通信的一个共享文件,又名pipe文件。写进程以字符流形式将大量数据写入管道,然后读进程从管道读出数据
进程间信号量的控制
《王道2021操作系统》P80
为了协调进程之间的相互制约关系,引入了进程同步的概念。
包括以下三个概念:
- 临界资源:系统中有些资源,可以被共享,但是同一时间只能有一个进程使用,这种资源称为临界资源。如打印机,变量数据等。对临界资源的访问,必须互斥进行,访问临界资源的代码称为临界区。
- 同步:同步也称为直接制约关系,是指为了完成某种任务而建立的两个或多个进程。这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系源于它们的合作。 如,A进程通过缓冲区向B进程发送数据。若缓冲区为空,则B必须等待A向缓冲区写入数据。若缓冲区满,则A必须等待B读出缓冲区的内容。
- 互斥:互斥也称为间接制约关系。当一个进程进入临界区使用临界资源时,其他进程若想使用该临界资源就必须等待。需要遵循以下几个原则:①空闲让进:若临界资源空闲,可以允许一个进程使用。②忙则等待:若临界资源正在被占用,则其他进程就需要等待。③有限等待:应保证等待的进程能在一定时间内使用到临界资源。④让权等待:等待的进程应把CPU让出来。
信号量
《王道2021操作系统》P84
信号量机制是用来解决互斥和同步问题的。可以把信号量理解为一个变量S,它只被两个标准的原语wait(S)和signal(S)访问,也可记为“P操作”和“V操作”。
信号量有以下几种:
1.整型信号量:信号量是一个整型变量,用于表示该资源剩余数量。对应的wait和signal代码为:
wait(S) {
while(S<=0);
S=S-1
}
signal(S) {
S=S+1
}
该机制未遵循“让权等待”原则。等待的程序会在while那里一直占用CPU
2.记录型信号量:在整型信号量的基础上,增加链表L,让所有等待的进程都链接起来。同时使用block和wakeup函数代替while。解决“忙等”现象,代码如下:
typedef struct{
int value;
struct process *L;
} semaphore;
wait(semaphore S){
S.value--;
if(S.value<0){
add this process to S.L;
block(S.L);
}
}
signal(semaphore S){
S.value++;
if(S.value<=0) {
remove a process P from S.L;
wakeup(P);
}
}
3.利用信号量来实现互斥
semaphore S=1;
P1(){
...
P(S);
进程1的临界区;
V(S);
}
P2(){
...
P(S);
进程2的临界区;
V(S);
}
该代码利用信号量,保证同一时间只有一个进程在访问临界资源。
各种并发编程模式
《CSAPP》P682
- 基于进程的并发编程:在父进程中接受客户端连接请求,然后创建一个新的子进程来为每个新客户端提供服务。这个缺点很明显,相当于为每个用户开辟一个子进程,那要是同时有十万个用户,那服务器就炸了
- I/O多路复用:基本思想为:使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。也就是单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流。 它的优点为:①它比基于进程的设计给了程序员更多对程序行为的控制。②所有请求都是运行在一个进程上下文中的,所以可以共享该进程的全部地址空间,请求之间共享数据更容易。它的缺点为:编码复杂
- 基于线程的并发编程:线程(Thread)就是运行在进程上下文中的逻辑流。它是上述两种方法的混合。进程可以创建多个线程来应对多个请求。线程由内核自动调度。每个线程都有它自己的线程上下文,包括唯一的线程ID、栈、程序计数器等。一个进程里的所有线程都可以共享其进程的整个虚拟空间。
共享变量和线程同步
《CSAPP》P698
共享变量的概念:若一个变量被多个线程同时引用,则该变量就是共享的。在Java中,多个线程同时去读写一个全局变量,那么这个全局变量就是一个共享变量。
线程同步:若一个共享变量同时被多个线程引用,那么就可能存在并发问题。即一个线程对变量的修改可能会覆盖另一个线程对该变量的修改。 要解决该问题,可以使用线程同步,即使用信号量(P,V操作),来限制变量同一时间只能被一个线程访问。在Java中,可以使用sychronized来为共享变量增加同步信号。
其他并行(发)问题
《CSAPP》P716
- 线程安全:函数有线程安全的和线程不安全的两种。如在Java中,HashMap是线程不安全的,HashTable是线程安全的。这个主要是函数的实现方式决定的。 若多个线程操作线程安全的对象,那么这个对象的结果一定是正确的。若多个线程操作同一个线程不安全的对象,那么最终可能会产生错误的结果。
- 可重入函数:可重入函数是线程安全的函数。若一个函数体中,没有使用共享变量,那么它就是可重入函数。可重入函数还分为显式可重入和隐式可重入。①显式可重入:fun(int a),该函数很明显用的一定是本地变量,那么这个函数就是显式可重入的。②隐式可重入:fun(int *p),因为该函数接收是一个指针类型,所以若调用它的函数传递的是一个本地变量,那么这个函数就是可重入函数,若是传了一个共享变量,那么该函数就是线程不安全的,那么就是一个不可重入函数。所以这种情况的函数称为隐式可重入。
- 竞争:若一个程序的正确性必须依赖线程之间的严格顺序执行,那么这个线程就会发生竞争。比如,必须线程A先执行到第5行代码,线程B才能执行第十行代码,否则结果就会不正确。 发生竞争是因为没有做到该条准则:多线程的程序必须对任何可行的轨迹都正确工作。
- 死锁:死锁是指一组线程被阻塞了,等待一个永远也不会为真的条件。一般发生死锁都是因为代码写的有问题。如果每个线程都是以一种顺序获得互斥锁并以相反顺序释放,那么这个程序就是无死锁的。