01静态链接 :01将可重定位的文件和命令行变成完全链接的、可加载、可运行的目标文件;02可重定位目标文件由各代码和数据节组成;
完成静态链接,链接器要完成以下两个工作:
1)符号解析,将每一个符号引用正好和一个符号定义关联起来;
2)重定位:可重定位的目标文件地址都是从零开始的,连接器通过吧每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得他们指向这个内存位置。
02目标文件:分类 可重定位目标文件;可执行目标文件;共享目标文件;
03符号和符号表:
定义:符号表记录了目标模块定义的符号和引用的符号信息。三种符号类型:
1.由模块m定义并能被其他模块引用的全局符号,非静态的C函数和全局变量;
2.由其他模块定义并被模块m引用的全局变量;
3.由目标模块定义和引用的局部符号,表现为静态全局变量和函数;
符号解析:链接符号引用与符号定义
a.全局符号的多重定义问题
强符号:已被初始化的全局变量和函数
弱符号:未被初始化的全局变量
b.规则
1:不允许有多个同名的强符号
2:如果有一个强符号和多个弱符号同名,选强符号
3:如果有多个弱符号同名,从其中任选一个
c.符号的地址由链接器确定,但符号的大小以及其类型在编译器就已经确定了,,链接器只负责解析和重定位符号确认符号的地址,同名符号有且仅有一个地址
d.静态链接可选方式:
1)一组可重定位目标文件
2)所有相关的目标模块打包成一个单独文件---静态库(存档文件)
e.使用静态库来解析符号
1)一个特殊现象:
一个静态库被解析后需后面的静态库使用若,,,
2)过程:符号解析时,链接器从左到右按照他们在编译器驱动程序命令行上出现的顺序来烧描可重定位目标文件和存档文件(如.c--.o)。链接器维护三个集合:所有目标模块集合E;未定义的集合U;定义的集合D;
烧描开始,链接器来判断输入f是什么,若是目标文件,f添加到E,修改U/D来反应f中的符号定义和引用;若f是存档文件,链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号,若存档文件成员m中有已定义的符号,m放E,修改U/D;如果链接器完成扫描后,U非空,则链接器输出错误并终止,否则,它会合并和重定位E中的目标文件;
04重定位和加载
01) 重定位 (可结合静态链接的两个步骤作答)
需要的术语:
a.重定位节和符号定义:聚合所有目标文件的相同节,链接器开始将运行时内存地址赋给每一个节和每一个符号 ☆☆☆
b.重定位节中的符号引用,链接器修改代码节和数据节中的每个符号引用,使得他们执行正确的运行时地址。
02)过程:
a.一个概念,当汇编器遇到未知的符号引用,就会生成一个重定位条目,代码的重定位条目存放在 . rel.text中,已初始化数据的重定位条目放在.rel.data中
重定位结构的结构:
typedef struct{ long offset; //符号相对节的偏移 long type:32; symbol:32; long addend; }Elf64_Rela;
b.符号引用的重定位
只讲两种基本的重定位类型:
01相对引用
02绝对引用
03)加载 ☆☆☆
1)背景:elf可执行文件的格式跟可重定位的格式是很相似的
2)可执行目标文件的加载
加载器将目标文件的代码和数据复制到内存中,然后跳转到入口点来运行即_start函数的地址;
05 动态链接库
1)出现的原因:为了解决静态库维护还是相对麻烦以及很多共用的库在内存中有很多碎片造成的内存浪费。
2)动态链接:共享库在加载或运行时加载到任意内存位置,并和在内存中的程序链接起来,由 动态链接器完成。
3)相关细节:
动态链接不会复制共享库的代码和数据段,仅会复制一些重定位信息和符号表;
信息和符号表
共享库中会有一个 .interp节,这个节包含动态链接器的路径名,动态链接器本身就是一个共享目标如(ld-linux.so)。
当运行一个可执行文件时,加载器会通过.interp节的信息找到动态链接器来运行,而动态链接器重定位相关的.so来完成链接任务:
位置无关代码PIC(position independent code):共享库若想要被进程共享就要求使用位置无关代码,位置无关代码是可以加载到内存的任何位置,而无需链接器修改即可以加载和重定位;
06 异常和进程
异常就是控制流突变,用来响应处理器状态中的某个变化,一部分由硬件实现,一部分
由操作系统来实现,每个异常都会被分配一个异常号,一些由硬件设计者来分配,常
见的有:内存缺页,算术溢出,内存访问违规,除零,一些由内核设计者分配,常用的
有系统调用和外部I/o设备的信号,异常有如下分类:中断,故障,陷阱,终止。
其中中断是异步发生的,中断函数处理结束后返回下条指令,陷阱是同步的总是返回
下条指令,故障时同步的,由潜在可恢复的错误造成(缺页),返回当前的指令,终止
同步的,不可恢复,不会返回。
28/539
a.异常处理写过程调用的一些不同之处
01返回地址不一样,异常返回fizz需要根据异常类型来确定
02异常处理时,处理器将一些额外的处理状态压入栈中
03若是由用户态进入内核态,则异常处理便用内核栈
04异常处理程序运行在内核态
b.四种不同类型
中断:不是由一条专门的指令造成的,它是处理器在每次执行一条指令后检测是否有中断产生:
陷阱和系统调用:陷阱是有意义的异常,像中断处理程序一样,陷阱处理程序将返回下一条指令。陷阱最重要的作用是
在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。系统调用都有一个服务号,指令“syscall n”来请求服务n;
,
中断与系统调用的区别:系统调用采用陷阱门,中断进入中断服务cpu自动关闭中断IF清0,防止嵌套;陷阱门进入服务程序时
IF不变,是开中断下进行的,所有系统调用可被中断;
系统调用与一般程序区别:调用的栈不同,一个运行在内核模式一个运行在用户模式;
故障:若故障能够被恢复,则返回到引起故障的指令,否则终止;
终止:不可恢复的错误。
进程:
a.定义:一个执行中的程序的实例。
进程与程序的区别:程序是一堆代码可以作为目标文件存在于磁盘上,或者作为段存在于地址空间中,进程是执行程序的一个具体实例,程序总是运行在某个进程的上下文中。
b.进程提供应用程序的关键抽象:
一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
c.逻辑控制流
PC值的序列-逻辑流
d.用户模式下访问内核
唯一的模式就是通过异常(中断、故障、陷阱)
处理器通常是用某个控制寄存器中的一个模式位,该寄存器描述了当前进程享有的特权。当设置了模式位时,进程就运行
在内核模式中(也叫超级用户模式)一个运行在内核模式的进程可以执行指令集中的任何指令,并可以访问系统中的
任何内存位置。没有设置,就是普通用户模式,不允许执行特权指令,比如停止处理器等;
e.上下文切换过程
每个进程维持一个上下文,上下文是内核重新启动一个被抢占的进程所需的状态。
过程:1保存当前进程的上下文,2恢复某个先前被抢占的进程被保存的上下文,3将控制传递给这个新恢复的进程。
7进程控制和信号
1.进程控制
a.获取进程号(进程有唯一的进程号 正数 ID/PID)
getpid;
b.创建进程和终止进程
程序员角度分三种
1运行:进程在cpu运行或等待运行且最终被内核调度。
2停止:进程的执行被挂起,且无触发条件不会被调度。
3终止:永远停止了,如收到一个信号终止了进程,或调用exit;
4创建:fork、vfork;
fork:调用一次返回两次,和父进程用相同的地址空间,与父进程并发执行,与父共享文件,pid不一样;
c.回收子进程
一个终止的进程若没有被父进程回收,会变成僵死进程,仍然会占用内存空间,直到被回收
若其父进程先终止,则另init进程收养、回收。init进程pid为1,是所有进程的祖先。
d.进程的休眠
sleep pause;
2.信号
a.一次软件形式的异常,一个信号就是一条消息,他通知进程系统中发生了一个某种类型
的事件。
底层的硬件异常是由内核异常处理程序处理,一般情况对用户而言是不可见的,但信号提供
了一种机制告诉用户进程系统发生了什么样的异常。
b.发生信号的原理:内核改变目的进程的上下文中某个状态来传递一个信号给目的进程;
(收发信号可简理解为:上下文中存与取)
接收信号后常见的信号处理行为有以下几种:
1进程终止:SIGKILL
2进程终止并转储内存
3进程停止(挂起)知道被SIGXONT信号重启
4进程忽略该信号
c.进程组的概念,每个进程都属于一个进程组,且父子进程同属于一个进程组。
d.阻塞和解除阻塞
隐式阻塞:阻塞任何与当前正在处理的信号类型的待处理的信号。
显式阻塞:明确的阻塞和解除阻塞选定的信号。
e.非本地跳转
一种用户级异常控制流,他可以将控制直接从一个函数转移到另一个当前正在执行的函数而不用通过函数栈机制
8.进程间的通信
指在不同的进程之间传播和交换信息
常见的通信手段:
1、管道及有名管道 :管道用于有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,有名管道
可用于无亲缘关系进程间通信。
2、信号:用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发信号给进程本身。
3、消息队列:是消息的链表,存放在内核中,并由消息队列标志符标示。克服了信号传输信息少,管道只能承载无格式字节
及缓存区大小受限等问题;
4信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问;
5共享内存:多个进程可以访问同一块内存空间,是最快的ipc形式,一般结合信号量使用;
6套接字:更一般的进程间通信机制,可以完成本机或者跨机器的进程通信;
9.进程间信号量的控制
信号量是一个计数器,是一个具有非负整值的全局变量,能做如下两个操作:
P(s):如果s为非零,那么P将s减1,并且立即返回。如果s为零,那么就挂起这个线程,直到
s变为非零,而一个V操作会重启这个线程。重启后P操作将s减1,并将控制返回给调用者。
V(s):V操作将s加1,如果有任何线程阻塞在P操作等待s变成非零,那么V操作会重启这些
线程中的一个,然后该线程将s减1,完成它的P操作。
10.信号量
信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行P和V操作,最简单
的信号量是只能取0和1的变量,这也是信号量最常用的一种形式,叫做二进制信号量。而可以取多个
常亮的信号量被称为通用信号量;
11 各种并发编程模式
概念:如果逻辑控制流在时间上有重叠,那它们就是并发的;
场景:
1访问慢速I/O设备
2与人交互
3推迟工作以降低延迟
4服务多个网络客户端
5在多核机器上并行计算
三种并发编程模式
1基于进程的并发编程
每个逻辑控制流都是一个进程,由内核来调度和维护
优缺点:
优点:共享文件表,不共享用户空间,更加的安全。
缺点:开销大,共享信息困难,要使用显式的IPC
2基于I/O多路复用的并发编程
基本的思想就是使用select函数,要求挂起进程,只有在一个或多个I/O事件发生后,
才将控制返回给应用程序;
I/O多路复用可以用做并发事件驱动程序的基础。
优缺点:
优点:程序员能更好的控制程序,共享数据简单
缺点:编码复杂,并且程度越小,复杂度越高;
3基于线程并发模型
概念:线程就是运行在进程上下文中的逻辑流,线程也由内核调度,每个线程都有自己的线程上下文,包括线程ID、栈、栈指针、程序计数器、条件吗和通用目的寄存器。
线程的切换比进程快很多,线程是对等的,任何线程都可以访问共享虚拟内存的任何位置。
分离线程
线程要么是可结合的,要么是可分离的,区别:
1可结合线程:可以被其他线程回收和杀死,被回收之前,它所占有的资源不会被释放
2可分离线程:不可以回收不可以杀死,它的内存资源在其终止时自动释放,默认创建的都是可结合线程;
12 共享变量和线程同步
1)线程内存模型:
每个线程都有自己独立的线程上下文、包括线程ID、栈栈指针、程序计数器、条形码和通用目的寄存器。
线程与其他线程共享进程上下文的剩余部分。包括整个用户虚拟地址空间。
2)将变量映射到内存
全局变量:定义在函数之外的变量,运行时,全局变量只有一个实例,任何线程都可以引用。
本地自动变量:定义在函数内,但没有用static属性的变量,运行时每个线程的栈都包含它自己的
所有本地自动变量实例。
本地静态变量:函数内部用static属性的变量,运行时只有一个实例。
3)共享变量
我们说一个变量v是共享的,当且仅当它的一个实例被一个以上的线程引用。如果只有一个线程引用就
不是共享的。共享变量是简单的,但这种共享方式会引来同步错误,因为线程之间的运行是竞争关系,
没办法确认进程运行的顺序,为了解决这种错误,可以使用信号量来对共享资源加锁,使其确定线程对该
变量的互斥访问。
4)线程同步
确保每个线程在执行它的临界区中的指令时,拥有对共享变量的互斥访问。
通过对二元信号量的使用(二元信号量也称为互斥锁),对共享资源进行加锁,使得每次只会有一个
线程能够访问,直到访问完成,其他线程才能访问,这样就能对资源的互斥旺旺,达到线程同步。
13 其他并行问题
1)线程安全:如果一个函数被称为线程安全的,那 当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。
四种线程不安全函数:
1不保护共享变量的函数
2保持跨越多个调用的状态的函数。
3返回指向静态变量的指针函数。
2)可重入性
有一类重要的线程安全函数,叫可重入函数,当它们被多个线程调用时,不会引用任何共享数据,尽管
线程安全和可重入不等价,可重入函数属于线程安全函数;
显式可重入函数:不依赖调用者,所有的数据引用都是本地自动栈变量,且参数是传值传递的(不是指针);
隐式可重入函数:形参可以使指针,小心的传递指向非共享数据的指针;
3)LINUX提供不安全的可重入版本(安全)函数名多以_r结尾;
4)竞争:当程序的结果依赖一个线程要在另一个线程达到y点前到达它的控制流中的x点时,就会产生竞争;
5)死锁:一个线程阻塞了,等待一个永远不会为真的条件。
--
---------------------