第七章 链接
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。通过学习链接这个章节,我得知了一下几个好处:
1.理解链接器将帮助你构造大型程序;
2.理解连接器将帮助你避免一些危险的编程错误;
3.理解链接将帮助你理解语言的作用域规则是如何实现的;
4.理解链接将帮助你理解其他重要的系统概念。
本章节大致讲述了以下几个内容:
*编译器驱动程序:
静态链接:
为了构造可执行文件,链接器必须完成两个主要任务:
1,符号解析。目标文件定义和引用符号,每个符号对应于一个函数,一个全局变量或一个静态变量。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。
2,重定位。编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,是的它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
*符号、符号表和符号解析:
*每个可重定位目标模块m有一个符号表,它包含m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:
1,由模块m定义并能被其他模块引用的全局符号。
2,由其他模块定义并被模块m引用的全局符号。
3,只被模块m定义和引用的局部符号。
符号表是由汇编器构造的,使用编译器输出到汇编语言,s文件中的符号。symtab节中包含ELF符号表。这张符号表包含一个条目的数组。下面这个程序展现了每个条目的格式:
typedef struct{
int name; /String table offset/
char type4; /Function or data/
blanding4; /Local or global/
char reserved; /Unused/
short section; /Section header index/
long value; /Section offset or absolute address/
long size; /Object size in bytes/
}Elf64_Symbol;
name是字符串表中的字节偏移,指向符号的一null结尾的字符串名字。value是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。size是目标的大小(以字节为单位)。type通常要么是数据,要么是函数。
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。对那些和引用定义在相同模块中的局部符号的引用,符号解析是非常解析明了的。编译器只允许每个模块中每个局部符号有一个定义。静态局部表量也会有本地链接器的符号,编译器还要确保他们拥有唯一的名字。
重定位
1、什么是重定位?
由于出现1这样的问题,就需要使用重定位这种方式解决上面的问题了。那什么是重定位呢?重定位就是在链接地址跟运行地址不同的情况下,执行一段位置无关码,这段位置无关码的作用就是将原来的那份代码全部复制到链接地址那里去,然后自己再长跳转到新的那份代码的刚刚执行的那个位置。这样就实现了链接地址跟运行地址一致的情况了。
2、为什么需要重定位?
就是链接地址跟运行地址不同,在这个情况下我们可以有两种方案:
①全部使用位置无关码。
②进行重定位让这两个地址相同。
我们知道,如果是一个小代码,使用①时可以的,但是一个大的代码文件很难保证全部都使用位置无关码的,这也是不现实的,所以必须使用重定位解决这个问题。
第八章 异常控制流
异常控制流(ECF)发生在计算机系统的各个层次。作为程序员,理解ECF很重要,这有很多原因:
1,理解ECF将帮助你理解重要的系统概念;
2,理解ECF将帮助你理解应用程序是如何与操作系统交互的;
3,理解ECF将帮助你编写有趣的新应用程序;
4,理解ECF将帮助你理解并发;
5,理解ECF将帮助你理解软件异常如何工作。
本章主要讲述了一下内容:
异常:
异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。异常就是控制流中的突变,用来响应处理器状态中的某些变化。异常可以分为四类:中断,陷阱,故障和终止。
进程:
进程(Process)是指计算机中已运行的程序,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。进程是程序真正运行的实例,若干进程可能与同一个程序相关,且每个进程皆可以同步或异步的方式独立运行。
进程状态:
创建进程
1.引起创建进程的事件
在多道程序环境中,只有(作为)进程(时)才能在系统中运行。因此,为使程序能运行,就必须为它创建进程。导致一个进程去创建另一个进程的典型事件,可以有以下四类:
1) 用户登录
在分时系统中,用户在终端键入登录命令后,如果是合法用户,系统将为该终端建立一个进程,并把它插入到就绪队列中。
2)作业调度
在批处理系统中,当作业调度程序按照一定的算法调度到某作业时,便将该作业装入到内存,为它分配必要的资源,并立即为它创建进程,再插入到就绪队列中。
3) 提供服务
当运行中的用户程序提出某种请求后,系统将专门创建一个进程来提供用户所需要的服务,例如,用户程序要求进行文件打印,操作系统将为它创建一个打印进程,这样,不仅可以使打印进程与该用户进程并发执行,而且还便于计算出为完成打印任务所花费的时间。
4) 应用请求
在上述三种情况中,都是由系统内核为它创建一个新进程,而这一类事件则是基于应用进程的需求,由它创建一个新的进程,以便使新进程以并发的运行方式完成特定任务。
进程的创建过程
一旦操作系统发现了要求创建新进程的事件后,便调用进程创建原语create()按下述步骤创建一个新进程。
1,申请空白PCB。为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB。
2,为新进程分配资源。为新进程的程序和数据以及用户栈分配必要的内存空间。显然,此时操作系统必须知道新进程所需要的内存大小。
3, 初始化进程控制块。PCB的初始化包括:
①初始化标识信息,将系统分配的标识符和父进程标识符,填入新的PCB中。
②初始化处理机状态信息,使程序计数器指向程序的入口地址,使栈指针指向栈顶。
③初始化处理机控制信息,将进程的状态设置为就绪状态或静止就绪状态,对于优先级,通常是将它设置为最低优先级,除非用户以显式的方式提出高优先级要求。
4, 将新进程插入就绪队列,如果进程就绪队列能够接纳新进程,便将新进程插入到就绪队列中。
进程的终止过程
如果系统发生了上述要求终止进程的某事件后,OS便调用进程终止原语,按下述过程去终止指定的进程。
1)根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程状态。
2)若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真。用于指示该进程被终止后应重新进行调度。
3)若该进程还有子孙进程,还应将其所有子孙进程予以终止,以防他们成为不可控的进程。
4)将被终止的进程所拥有的全部资源,或者归还给其父进程,或者归还给系统。
5)将被终止进程(它的PCB)从所在队列(或链表)中移出,等待其它程序来搜集信息。
阶段
进程是由进程控制块、程序段、数据段三部分组成。一个进程可以包含若干线程(Thread),线程可以帮助应用程序同时做几件事(比如一个线程向磁盘写入文件,另一个则接收用户的按键操作并及时做出反应,互相不干扰),在程序被运行后,系统首先要做的就是为该程序进程建立一个默认线程,然后程序可以根据需要自行添加或删除相关的线程。是可并发执行的程序。在一个数据集合上的运行过程,是系统进行资源分配和调度的一个独立单位,也是称活动、路径或任务,它有两方面性质:活动性、并发性。进程可以划分为运行、阻塞、就绪三种状态,并随一定条件而相互转化:就绪–运行,运行–阻塞,阻塞–就绪。
进程为应用程序的运行实例,是应用程序的一次动态执行。看似高深,我们可以简单地理解为:它是操作系统当前运行的执行程序。在系统当前运行的执行程序里包括:系统管理计算机个体和完成各种操作所必需的程序;用户开启、执行的额外程序,当然也包括用户不知道,而自动运行的非法程序(它们就有可能是病毒程序)。
信号:
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。每种信号类型都对应于某种系统事件。
信号术语
传送到一个信号到目的进程是由两个不同步骤组成的:
1,发送信号。内核通过更新目的进程上下文中的某个状态,发送一个信号给目的的进程。
2,接收信号。当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。
发送信号
1.进程组:
每个进程都只属于一个进程组, 默认地,一个子进程和它的父进程同属于一个进程组。
getpgrp: 获取当前进程的进程组ID
setpgid: 将进程pid的进程组改为pgid。若pid为0,就使用当前进程的pgid。若pgid为0,就用pid作为进程组ID.
#include
pid_t getpgrp(void); 返回:调用进程的进程组ID
int setpgid(pid_t pid, pid_t pgid); 返回: 若成功则为0,否则为-1
2./bin/kill程序发送信号:
可向另外的进程发送任意信号.
/bin/kill -9 15213
发送信号9(SIGKILL)到进程15213。一个负PID会导致信号被发送到进程组PID中的每个进程.
3.从键盘发送信号
输入ctrl-c会使内核向每个前台进程组中的成员发送一个SIGINT信号。
4.用kill函数发送信号
进程通过调用kill函数发送信号给其他进程(包括它自己).
如果pid>0,那么kill函数发送信号sig给进程pid.若pid<0,那么kill发送信号sig给进程组abs(pid)中的每个进程.
5.用alarm函数发送信号
进程可调用alarm向它自己发送SIGALRM信号.
#include
unsigned int alarm(unsigned int secs); 返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0
alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程.
接收信号
每个信号类型都有一个预定义的默认行为,进程可调用signal函数修改信号的默认行为.但SIGSTOP和SIGKILL是不能修改的.
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
返回:若成功则为指向前次处理程序的指针,若出错则为SIG_ERR
signal修改信号signum的行为的三种方法:
handler为SIG_IGN: 忽略信号
handler为SIG_DFL: 恢复默认行为
handler为用户自定义
第十章 系统级I/O
学习系统级I/O的目的:
1,了解UnixI/O将帮助你理解其他的系统概念;
2,有时你除了使用UnixI/O以外别无选择
UnixI/O:
一个Linux文件就是一个m个字节的序列:
B0,B1,B2,…,Bk,…,Bm-1
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
-打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。
-Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入,标准输出和标准错误。
-改变当前的文件位置。
-读写文件。
-关闭文件。
文件:
每个Linux文件都有一个类型来表明它在系统中的角色:
1,普通文件包含任意数据;
2,目录是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录。
3,套接字是用来与另一个进程进行跨网络通信的文件
*打开和关闭文件,读和写文件:
1.open 函数:(open 系统调用用来打开或创建一个文件)
函数原型为:
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname : 是要打开或创建的含路径的文件名
flags : 打开文件的方式
O_RDONLY : 只读方式打开文件
O_WRONLY : 只写方式打开文件
O_RDWR ; 可读可写方式打开文件
(这三种打开方式互斥,但是它们可以分别与下列标志进行或运算)
O_CREAT : 文件不存在自动创建文件,只有此时用到第三个参数mode说明新文件的存取权限
O_EXCL : 如果O_CREAT也被设置,此指令会检查文件是否存在.不存在则创建该文件,存在则导致打开文件错误.
O_TRUNC : 若文件存在且以可写方式打开,原文件保存数据将丢失,但文件属性不变.
O_APPEND : 所写入的数据会以追加的方式添加到文件后面.
O_SYNC : 已同步方式打开文件,任何对文件的修改都会阻塞直到物理磁盘上的数据同步以后才返回.
当且仅当第二参数使用了O_CREAT时,新文件的实际存取权限是mode和umask按照(mode& ~unmask)运算以后的结果.
成功调用O_CREAT函数会返回一个文件描述符,若有错误会返回-1.
2.creat 函数(完成文件的创建)
函数原型:
#include
#include
#include
int creat(const char *pathname, mode_t mode);
pathname:要打开或创建的文件名,若指向的文件不存在,则创建一个新文件,若存在,则新文件覆盖原文件.
成功调用creat会返回一个文件描述符,若有错误会返回-1.
(注:creat只能以只写的方式打开创建的文件,creat无法创建设备文件,设备文件的创建要使用mknod函数)
3.close函数(用来关闭一个打开的文件)
函数原型:
#include
int close (int fd);
fd:需要关闭的文件的文件描述符.
当close 调用成功返回0,发生错误会返回-1.
(注:close函数调用成功时并不能保证数据可以全部写回硬盘)