总结
前几周总结博客以及实验:
http://www.cnblogs.com/20135331wenyi/p/5005311.html
http://www.cnblogs.com/20135331wenyi/p/5042618.html
http://www.cnblogs.com/20135331wenyi/p/5029538.html
http://www.cnblogs.com/20135331wenyi/p/5005311.html
http://www.cnblogs.com/20135331wenyi/p/4986042.html
http://www.cnblogs.com/20135331wenyi/p/4967158.html
http://www.cnblogs.com/20135331wenyi/p/4967158.html
http://www.cnblogs.com/20135331wenyi/p/4967158.html
一、异常
异常一部分是由硬件实现的,一部分是由操作系统实现的。(*不是单独的某一方实现)
异常表和异常处理程序
异常的四种类别:
- 中断
- 陷阱和系统调用
- 故障
- 终止
这四种中除了中断是异步的,其余三种都是同步的。
中断是向来自处理器外部的I/O设备的信号的结果,陷阱是执行一条指令的结果,可以实现系统调用,这两种异常是时常被我们利用的;
故障和终止都是由于错误情况引起,区别在这些错误能否被恢复。
从而,需要了解Linux/IA32系统中的异常,比如常用的异常号对应的事件,系统调用号和例子,等等。
二、进程
进程是计算机操作系统里的三个重要抽象之一,是对一个正在运行的程序的抽象,经典定义为一个执行中的程序的实例。
在进程的概念部分,需要理解的是逻辑控制流,并发,私有地址空间,用户模式和内核模式,上下文切换,等等概念。
在进程切换部分,有几个常用函数:
一、获取进程ID
每个进程都有一个唯一的正数进程ID(PID)。
#include <sys/types.h>
#includepid_t getpid(void); 返回调用进程的PID
pid_t getppid(void); 返回父进程的PID(创建调用进程的进程)二、创建和终止进程
1.进程总是处于下面三种状态之一
- 运行
- 停止:被挂起且不会被调度
终止:永远停止。原因:
1.收到信号,默认行为为终止进程
2.从主程序返回
3.调用exit函数2.创建进程
父进程通过调用fork函数来创建一个新的运行子进程。fork函数定义如下:
#include <sys/types.h>
#includepid_t fork(void);
fork函数只被调用一次,但是会返回两次:父进程返回子进程的PID,子进程返回0.如果失败返回-1.
- 调用一次,返回两次
- 并发执行,内核能够以任何方式交替执行它们的逻辑控制流中的指令
相同和不同:
相同:用户栈、本地变量值、堆、全局变量值、代码
不同:私有地址空间共享文件:子进程继承了父进程所有的打开文件。参考10.6节笔记。
调用fork函数n次,产生2的n次方个进程。
3.终止进程
用exit函数。
#include
void exit(int status);
exit函数以status退出状态来终止进程。
三、回收子进程
进程终止后还要被父进程回收,否则处于僵死状态。
如果父进程没有来得及回收,内核会安排init进程来回收他们。init进程的PID为1.
一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。waitpid函数的定义如下:
#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);
成功返回子进程PID,如果WNOHANG,返回0,其他错误返回-1.
四、让进程休眠
1.sleep函数
sleep函数使一个进程挂起一段指定的时间。定义如下:
#include
unsigned int sleep(unsigned int secs);
返回值是剩下还要休眠的秒数,如果到了返回0.
2.pause函数
#include
int pause(void);
让调用函数休眠,直到该进程收到一个信号。五、加载并运行程序——execve函数
#include
int execve(const char filename, const char argv[], const char *envp[]);
成功不返回,失败返回-1.
execve函数调用一次,从不返回。
- filename:可执行目标文件
- argv:参数列表
- envp:环境列表
getnev函数
#include
char getenv(const char name);
若存在则为指向name的指针,无匹配是null
在环境数组中搜寻字符串"name=value",如果找到了就返回一个指向value的指针,否则返回null。setenv和unsetenv函数
#include
int setenv(const char name, const char newvalue, int overwrite);
若成功返回0,错误返回-1void unsetenv(const char *name);
无返回值
如果环境数组包含"name=oldvalue"的字符串,unsetenv会删除它,setenv会用newvalue代替oldvalue,只有在overwrite非零时成立。如果name不存在,setenv会将"name=newvalue"写进数组。
※fork函数和execve函数的区别
fork函数是创建新的子进程,是父进程的复制体,在新的子进程中运行相同的程序,父进程和子进程有相同的文件表,但是不同的PID
execve函数在当前进程的上下文中加载并运行一个新的程序,会覆盖当前进程的地址空间,但是没有创建一个新进程,有相同的PID,继承文件描述符。
以上这些函数十分常用,尤其是fork函数和execve函数,需要搞懂在调用它们之后分别会有什么结果,谁返回什么,产生了什么东西。
三、信号
信号是一种更高层的软件形式的异常,它允许进程中断其他进程。
首先要知道Linux信号有固定名称和对应的默认行为、相应事件,然后是关于信号的两个术语:发送和接受。
发送信号有几种方法:
- 用/bin/kill
- 从键盘发送
- 调用kill函数
- 调用alarm函数
接收信号的相关过程,signal函数捕获。
(1)信号产生-四种类型
- 用户产生-Ctrl+C。
stty-a,查看哪些按键可以产生信号
- 硬件产生-除零错误
- 进程产生-kill指令
- 内核产生-闹钟超时
(2)信号处理-三种方法
- 执行默认操作
- 忽略信号
- 捕捉信号:执行信号处理函数,切换到用户态。
还有一个非常需要注意的地方:c语言中有关指针的四种:
指针数组
是数组,数组里的元素是指针
int *daytab[13]
数组指针
是指针,指向一个类型和元素个数都固定的数组
int (*daytab1)[13]
指针函数
是函数,返回值类型是指针
int *comp()
函数指针
是指针,指向函数的指针,函数名就是函数指针
int (*comp1)()
如何识别类型?——右左右左法
每次分析不要跨过括号,位于右边的小括号和中括号具有更高的优先级。
1.数组一定要告诉元素个数和数据类型
2.函数一定有形参和返回值类型
3.数组指针,函数指针,*和指针要括起来。
这四种要是分不清 ,就没办法正确编程和理解代码。
第九章 虚拟存储器
虚拟存储器也是操作系统中三个重要的抽象之一,这一章的具体内容在操作系统的课程中有详细的讲解。
最先要弄明白的就是物理地址和虚拟地址的区别,地址空间的概念,然后是虚拟存储器作为缓存的工具的组织形式,即页表:
页表是一个数据结构,存放在物理存储器中,将虚拟页映射到物理页。
页表就是一个页表条目PTE的数组,组成为:
有效位+n位地址字段
1.如果设置了有效位:
地址字段表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页
2.如果没有设置有效位:
(1)空地址:
表示该虚拟页未被分配
(2)不是空地址:
这个地址指向该虚拟页在磁盘上的起始位置。
页表条目PTE,要会读,明白这是什么意思。
然后是页命中,这个类比着缓存命中的思想来考虑,同时有页不命中、页替换等等,我们操作系统上这几节课正好在学这一部分,比本课课本更为详细一些。
另一个比较大也比较难的知识点是地址翻译:
地址翻译就是一个N元素的虚拟地址空间VAS中的元素和一个M元素的物理地址空间PAS中元素之间的映射。
页面基址寄存器PTBR指向当前页表。
MMU利用VPN选择适当的PTE。
PPO=VPO。
1.当页面命中时,CPU动作:
- 处理器生成虚拟地址,传给MMU
- MMU生成PTE地址,并从高速缓存/主存请求得到他
- 高速缓存/主存向MMU返回PTE
- MMU构造物理地址,并把它传给高速缓存/主存
高速缓存/主存返回所请求的数据给处理器。
2.处理缺页时:
- 处理器生成虚拟地址,传给MMU
- MMU生成PTE地址,并从高速缓存/主存请求得到他
- 高速缓存/主存向MMU返回PTE
- PTE中有效位为0,触发缺页异常
- 确定牺牲页
- 调入新页面,更新PTE
返回原来的进程,再次执行导致缺页的指令,会命中
课本上要求的练习程度只是根据虚拟地址空间和物理地址,计算出VPN,VPO,PPN,PPO的位数,在操作系统课程中还要求根据逻辑地址判断是否能够用物理地址表示,以及表示的结果是什么。原理简单说就是前几位是页号,后几位是物理块。
这章里还涉及到缺页异常和处理。
存储器映射中,主要是在讲共享和私有的关系,共享对象和私有对象怎么实现怎么应用,并且进一步对fork和execve函数进行了解释。
动态存储器分配同样是操作系统中讲述过的内容,用malloc等动态的,需要多少存储器就分配多少存储器,如果造成了碎片如何处理,等等。
垃圾收集器是一种动态存储分配器,最常用的是Mark&Sweep垃圾收集器:
ptr定义为typedef void *ptr
- ptr isPtr(ptr p):如果p指向一个已分配块中的某个字,那么就返回一个指向这个块的起始位置的指针b,否则返回NULL
- int blockMarked(ptr b):如果已经标记了块b,那么就返回true
- int blockAllocated(ptr b):如果块b是已分配的,那么久返回ture
- void markBlock(ptr b):标记块b
- int length(ptr b):返回块b的以字为单位的长度,不包括头部
- void unmarkBlock(ptr b):将块b的状态由已标记的改为未标记的
- ptr nextBlock(ptr b):返回堆中块b的后继
还有关于c语言保守的;
c程序中常见的与存储器有关的错误这一部分,有十个经常会犯的错误,与我们日常的编程息息相关,之前未学习时没有在意过的内容现在都要注意。
第十章 系统级I/O
这一章涉及到第三个重要抽象——文件
所有的I/O设备都被模型化为文件,而所有的输入输出都被当做对应文件的读写来进行,这其中涉及到几个概念:
- 文件描述符
- 标准输入、标准输出、标准错误
- EOF
- ……
对于文件的打开关闭和读写,有以下几个函数:
1.open函数
#include <sys/types.h>
#include <sys/stat.h>
#includeint open(char *filename, int flags, mode_t mode);
2.close函数
#include
int close(int fd);
- fd:即文件的描述符。
3.读 read
#include
ssize_t read(int fd, void *buf, size_t n);
4.写 write
#include
ssize_t write(int fd, void *buf, size_t n);
5.通过lseek函数可以显式的修改当前文件的位置
而由于不足值(不足值指在某些情况下,read和write传送的字节比应用程序要求的要少)的存在,需要用一个健壮的I/O包,即RIO包,来自动处理不足值,这样就有了健壮的读写,相应函数为:
1.RIO的无缓冲的输入输出函数。
这些函数的作用是直接在存储器和文件之间传送数据,常适用于网络和二进制数据之间。
rio_readn函数和rio_writen定义:
#include "csapp.h"
ssize_t rio_readn(int fd, void usrbuf, size_t n);
ssize_t rio_writen(int fd, void usrbuf, size_t n);2.RIO的带缓冲的输入函数
可以高效的从文件中读取文本行和二进制数据。
一个概念:一个文本行就是一个由换行符结尾的ASCII码字符序列。
范例:如何统计文本文件中文本行的数量——通过计算换行符。需要用到的函数:
#include "csapp.h"
void rio_readinitb(rio_t *rp, int fd);//将描述符fd和地址rp处的一个类型为rio_t的读缓存区联系起来。
ssize_t rio_readlineb(rio_t rp,void usrbuf, size_t maxlen);//从文件rp中读出一个文本行,包括换行符,拷贝到存储器位置usrbuf,并用空字符结束这个文本行。最多赌到maxlen-1个字节,最后一个给结尾的空字符。
ssize_t rio_readnb(rio_t rp, void usrbuf, size_t n);//从文件rp中读取最多n个字符到存储器位置usrbuf中。成功则返回传送的字节数,EOF为0,出错为-1。
之后涉及的读取文件元数据stat和fstat,区别只是在输入的是文件名还是文件描述符。
共享文件这个部分用了三个相关的数据结构来表示,分别是描述符表、文件表和v-node表,这三个表在后半学期的学习过程中多次使用,方便的理解了共享文件的特性,以及父子进程的特点——
他们共享相同的打开文件表集合,共享相同的文件位置。
第十二章 并发编程
首先我们需要明确关于并发的两个概念:
- 程序级并发——进程
- 函数级并发——线程
然后三种基本的构造并发程序的方法:
- 进程
- I/O多路复用
- 线程
这一章前半部分就主要讲解这三种方式构造并发程序的方法,最重要的是线程,它是前两种方法的混合。
首先要了解线程的执行模式:
1.主线程
在每个进程开始生命周期时都是单一线程——主线程,与其他进程的区别仅有:它总是进程中第一个运行的线程。
2.对等线程
某时刻主线程创建,之后两个线程并发运行。
每个对等线程都能读写相同的共享数据。
3.主线程切换到对等线程的原因:
- 主线程执行一个慢速系统调用,如read或sleep
- 被系统的间隔计时器中断
切换方式是上下文切换
对等线程执行一段时间后会控制传递回主线程,以此类推
4.线程和进程的区别
- 线程的上下文切换比进程快得多
- 组织形式:
- 进程:严格的父子层次
- 线程:一个进程相关线程组成对等(线程)池,和其他进程的线程独立开来。一个线程可以杀死它的任意对等线程,或者等待他的任意对等线程终止。
然后是关于线程的几个函数:
1.创建线程:pthread_create函数
#include
typedef void (func)(void );int pthread_create(pthread_t tid, pthread_attr_t attr, func f, void arg);
成功返回0,出错返回非0
2.查看线程ID——pthread_self函数
#include
pthread_t pthread_self(void);
返回调用者的线程ID(TID)
3、终止线程
a.终止线程的几个方式:
- 隐式终止:顶层的线程例程返回
- 显示终止:调用pthread_exit函数
*如果主线程调用,会先等待所有其他对等线程终止,再终止主线程和整个进程,返回值为pthread_return- 某个对等线程调用Unix的exit函数,会终止进程与其相关线程
- 另一个对等线程通过以当前线程ID作为参数调用pthread_cancle来终止当前线程
b.pthread_exit函数
#include
void pthread_exit(void *thread_return);
若成功返回0,出错为非0
c.pthread_cancle函数
#include
void pthread_cancle(pthread_t tid);
若成功返回0,出错为非0
4、回收已终止线程的资源
用pthread_join函数:
#include
int pthread_join(pthread_t tid,void **thrad_return);
这个函数会阻塞,知道线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源
在用信号量同步线程这一部分中,首先注意的是:
1.进度图
进度图的表示方法需要了解,最简单的说,合法的路径是向右向上,禁对角线禁左禁下。
然后同时这里还带进来几个概念,互斥和不安全区。
2.信号量
关于信号量P和V,在操作系统中已经有详细的教学过程。
而用信号量实现互斥和对共享资源的调度访问,有两个大的问题,分别是生产者-消费者问题,和读者-写者问题,也都是操作系统课程上的例题。
3.死锁
操作系统中专门有一张来讲这个,死锁的可能产生原因,怎么预防死锁,怎么计算安全序列,等等。
二、代码
在下半个学期中,老师隔周就会给一些代码让我们去阅读,这些代码有一些读起来比较容易,比如说在系统级I/O的时候,那些程序都是在模仿Linux的固有指令;而第八章的代码,一部分是在讲父子程序的问题,另一部分是一些功能相对更好用的一些函数,阅读起来有一定的困难。
鉴于这一部分不过是重复之前的笔记,就不复制粘贴了。
关于代码,还有一个重要的概念,叫做万能函数:
万能函数:
void func(void parameter)
typedef void (uf)(void para)即,输入的是指针,指向真正想要传到函数里的数据,如果只有一个就直接让指针指向这个数据,如果是很多就将它们放到一个结构体中,让指针指向这个结构体。后面这个方法就是万能函数的使用思想。
之所以叫万能函数,就是说所有的函数都可以转化为这种形式,而在系统的相关函数中也多用这种模式。
但是学习好万能函数的前提就是分清前面说的指针数组、数组指针、指针函数、函数指针。记忆技巧是,这四个次都是偏正短语,重点在后面。