Bryce1010的操作系统课程设计
https://download.csdn.net/download/fire_to_cheat_/10221003
上面是课程设计的代码,下载需要一些积分。
1.作业调度
2.磁盘调度
常见的磁盘调度算法大致分为以下5类:
FCFS、SSTF、SCAN、CSCAN、FSCAN
程序实现了上述5类调度算法。
其中,当前磁道和要求服务的磁道均由系统随机产生。
程序入口是main主函数,在程序一开始由request()函数产生随机的要求服务的磁盘序列。然后由用户选择算法FCFS、SSTF、SCAN、CSCAN、FSCAN其中之一。
分别执行相应的算法。
1)FCFS算法实现思路:将vector内随机产生的数依次读出,相当于对于队列数据结构中的出队操作。
2)SSTF算法实现思路:在时间复杂度和空间复杂度上的综合考虑,我首先将vector内的数据进行排序,然后确定当前磁道号在有序数据中的位置,然后在该位置的左右找到离它最近的数,并将当前位置进行刷新。
3)SCAN算法实现思路:首先将vector内的数据进行排序,然后同样地确定当前磁道号在有序数据中的位置,然后在向内的方向上依次访问,访问完了之后,再输出初始位置向外的服务序列。
4)CSCAN算法实现思路:开始和前面的算法一样,也是先进行排序,定位,然后在向内的方向上依次访问,访问完了之后,再从最外层向内扫。
5)FSCAN算法实现思路:将初始的序列放在一个队列中,将在扫描过程中新出现的服务序列放在另一个序列中,然后对两个队列中的数据依次进行SCAN算法的实现。
3.熟悉linux系统文件系统调用
1.open
所需头文件:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
函数原型:int open(const char *pathname,flags,int perms)
pathname:被打开的文件名,可包含路径
flag :文件打开的方式,参数可以通过“|” 组合构成,但前3 个参数不能互相重合。
文件权限标志:
参 数 | 说 明 | 参 数 | 说 明 |
---|---|---|---|
S_IRUSR | 所有者拥有读权限 | S_IXGRP | 群组拥有执行权限 |
S_IWUSR | 所有者拥有写权限 | S_IROTH | 其他用户拥有读权限 |
S_IXUSR | 所有者拥有执行权限 | S_IWOT | 其他用户拥有写权限 |
S_IRGRP | 群组拥有读权限 | S_IXOTH | 其他用户拥有执行权限 |
S_IWGRP | 群组拥有写权限 |
umask变量表示方法
加 权 数 值 | 第1位 | 第2位 | 第3位 |
---|---|---|---|
4 | 所有者拥有读权限 | 群组拥有读权限 | 其他用户拥有读权限 |
2所有者拥有写权限 | 群组拥有写权限 | 其他用户拥有写权限 | |
1所有者拥有执行权限 | 群组拥有执行权限 | 其他用户拥有执行权限 |
返回值,成功返回文件描述符,失败返回-1
需要注意一点:由于文件描述符0、1、2已经默认存在了,因此,新打开的文件的文件描述符是从3开始的。
2.close
函数原型:int close (int fd )
函数输入值:fd :文件描述符
函数返回值:成功:0 出错:-1
3.read
所需头文件:
#include<unisted.h>
函数原型:ssize_t read(int fd,void *buf,size_t count)
fd: 文件描述符
Buf :指定存储器读出数据的缓冲区
Count :指定读出的字节数
函数返回值:成功:读出的字节数 0 :已到达文件尾 -1 :出错
4.write
所需头文件
#include<unisted.h>
函数原型: ssize_t write(int fd,void *buf,size_t count)
函数传入值:
fd: 文件描述符
Buf :指定存储器写入数据的缓冲区
Count :指定读出的字节数
函数返回值:成功:已写的字节数 -1 :出错
5.lseek
所需头文件:
#include<unisted.h>
函数原型:off_t lseek(int fd,off_t offset,int whence)
函数传入值:
fd: 文件描述符
Offset :偏移量,每一读写操作所需要移动的距离,单位是字节的数量,可正可负(向前移,向后移)
Whence :当前位置的基点:
SEEK_SET :当前位置为文件开头,新位置为偏移量的大小
SEEK_CUR :当前位置为文件指针位置,新位置为当前位置加上偏移量
SEEK_END :当前位置为文件的结尾,新位置为文件的大小加上偏移量大小
4.进程管理
4.1预备知识
1.管道
管道是Linux支持的最初Unix IPC 形式之一, 具有以下特点:
①管道是半双工的, 数据只能向一个方向流动; 需要双方通信时, 需要建立起两个管道;
②只能用于父子进程或者兄弟进程之间( 具有亲缘关系的进程) ;
③单独构成一种独立的文件系统: 管道对于管道两端的进程而言, 就是一个文件, 但它不是普
通的文件, 它不属于某种文件系统, 而是自立门户, 单独构成一种文件系统, 并且只存在于内存中。
④数据的读出和写入: 一个进程向管道中写的内容被管道另一端的进程读出。 写入的内容每次
都添加在管道缓冲区的末尾, 并且每次都是从缓冲区的头部读出数据。
管道通过系统调用 pipe()来实现, 函数的功能和实现过程如下。
原型: int pipe( int fd[2] )
返回值: 如果系统调用成功, 返回 0。 如果系统调用失败返回-1:
errno = EMFILE (没有空闲的文件描述符)
ENFILE (系统文件表已满)
EFAULT (fd 数组无效)
注意: fd[0] 用于读取管道, fd[1] 用于写入管道
2.fork()
创建一个新进程。
系统调用格式:
Pid=fork()
参数定义:
Int fork()
fork()返回值意义如下:
0: 在子进程中, pid 变量保存的 fork()返回值为 0, 表示当前进程是子进程。
(>0): 在父进程中, pid 变量保存的 fork()返回值为子进程的 id 值( 进程唯一标识符) 。
-1: 创建失败。
如果 fork()调用成功, 它向父进程返回子进程的 pid, 并向子进程返回 0, 即 fork() 被
调用了一次, 但返回了两次。 此时 OS 在内存中建立一个新进程, 所建的新进程是调用 fork() 父进程
( parent process) 的副本, 称为子进程( child process) 。 子进程继承了父进程的许多特性,
并具有父进程完全相同的用户级上下文, 父进程与子进程并发执行。
内核为 fork()完成以下操作:
1) 为新进程分配一进程表项和进程标识符
进入 fork()后, 内核检查系统是否有足够的资源来建立一个新进程。 若资源不足, 则
fork()系统调用失败; 否则, 核心为新进程分配一进程表项和唯一的进程标识符。
2) 检查同时运行的进程数目
超过预先规定的最大数目时, fork()系统调用失败。
3) 拷贝进程表项中的数据
将父进程的当前目录和所有已打开的数据拷贝到子进程表项中, 并置进程的状态为“ 创建” 状
态。
4) 子进程继承父进程的所有文件
对父进程当前目录和所有已打开的文件表项中的引用计数加 1.
5) 为子进程创建进程上、 下文
进程创建结束, 设子进程状态为“ 内存中就绪” 并返回子进程的标识符。
6) 子进程执行
虽然父进程与子进程程序完全相同, 但每个进程都有自己的程序计数器 PC, 然后根据
Pid 变量保存的 fork()返回值的不同, 执行了不同的分支语句。
3.exec()系列
系统调用 exec()系列, 也可用于新程序的运行。 fork()只是将父进程的用户级上下文
拷贝到新进程中, 而 exec()系列可以将一个可执行的二进制文件覆盖在新进程的用户级上下
文的存储空间上, 以更改新进程的用户级上下文。 exec()系列中的系统调用都完成相同的功能, 它
们把一个新程序装入内存, 来改变调用进程的执行代码, 从而形成新进程。 如果
exec()调用成功后, 没有任何数据返回, 这与 fork()不同。 exec()系列调用在 LINUX 系统库
unistd.h 中, 共有execl、 execlp、 execv、 execvp 五个, 其基本功能相同, 只是以不同的方式来
给出参数。
一种是直接给出参数的指针, 如:
Int execl(path,arg0[,arg1,…argn],0);
Char *path,*argv[];
exec()和fork()联合使用
系统调用 exec()和 fork()联合使用能为程序开发提供有力支持。 用 fork()建立子进程,
然后再在子进程中使用 exec(), 这样就实现了父进程与一个和它完全不同子进程的并发执
行。
一般, wait、 exec 联合使用的模型为: Int
status;
……
If(fork()==0)
{ .
…..;
execl(…);
……;
}
Wait(&status);
4.wait()
等待子进程运行结束。 如果子进程没有完成, 父进程一直等待。 wait()将调用进程挂起, 直至其
子进程因暂停或终止而发来软中断信号为止。 如果在 wait()前已有子进程暂停或终止, 则调用
进程做适当处理后便返回。
系统调用格式:
Int wait(status)
Int *status;
其中, status 是用户空间的地址。 它的低 8 位反应子进程状态, 为 0 表示子进程正常
结束, 非 0 则表示出现了各种各样的问题; 高 8 位则带回了 exit()的返回值。 exit()返回值由
系统给出。
核心对 wait()作以下处理:
① 首先查找调用进程是否有子进程, 若无, 则返回出错码;
② 若找到一处于“僵死状态” 的子进程, 则将子进程的执行时间加到父进程的执行时间
上, 并释放子进程的进程表项;
③ 若未找到处于“ 僵死状态” 的子进程, 则调用进程便在可被中断的优先级上睡眠, 等
待其子进程发来软中断信号时被唤醒。
5.exit()
终止进程的执行。
系统调用格式:
Void exit(status)
Int status;
其中, status 是返回给父进程的一个整数, 以备查考。
为了及时回收进程所占用的资源并减少父进程的干预, LINUX/LINUX利用exit()来实现进程
的自我终止, 通常父进程在创建子进程时, 应在进程的末尾安排一条 exit(), 使子进程自我
终止。 exit(0)表示进程正常终止, exit(1)表示进程运行有错, 异常终止。
如果调用进程在执行 exit()时, 其父进程正在等待它的终止, 则父进程可立即得到其
返回的整数。 核心须为 exit()完成以下操作:
1) 关闭软中断
2) 回收资源
3) 写记账信息
4) 置进程为“僵死状态”
6.signal函数
下面我们就来看一下signal函数如何使用,这个函数用在程序中的时候,是为程序注册信号用的,函数有俩个参数,一个是信号的宏,就是SIGXXX形式的宏,这个宏不可以是SIGKILL和SIGSTOP信号,也就是数字9和19的信号,因为这俩个信号是不可以被忽略和自己重定义处理方式的。另一个参数可以有三种情况,可以是一个函数指针,在函数的实现里边可以写自己的处理方式;可以是SIG_IGN宏,代表该程序将忽略前边的那个类型的信号,当程序捕捉到这个信号的时候就不处理;可以是SIG_DFL,表示的是采用系统的默认处理方式,系统的默认处理方式一般都是中断信号的。这个函数没有返回值。现在我们就通过具体的代码来看看如何使用它吧。
#include <stdio.h>
#include <signal.h> //signal函数的头文件
#include <stdlib.h>
//信号处理函数,这个函数的参数必须是int类型的,用来接收信号,返回值是void
void sg(int sig)
{
printf("捕获了信号:%d\n",sig);
}
int main()
{
//注册了SIGINT信号,当该程序收到这个信号的时候,就会调用sg函数了,如果没有注册成功的话,就执行下边的语句了
if(signal(SIGINT,sg) == SIG_ERR)
perror("sigint"),exit(-1);
//注册SIGQUIT信号,当程序收到这个信号的时候,会忽略掉这个信号的,当然还可以注册为默认的类型,这个时候系统会
//采用默认的处理方式的
if(signal(SIGQUIT,SIG_IGN) == SIG_ERR)
perror("sigquit"),exit(-1);
//这里之所以加个死循环是因为上边的程序代码是用来注册的,当程序执行到这里的时候就不走了,以便我们给这个程序传递信号
while(1);
return 0;
}
7.sprintf函数简介
函数功能:把格式化的数据写入某个字符串
头文件:stdio.h
函数原型:int sprintf( char *buffer, const char *format [, argument] … );
返回值:字符串长度(strlen)
5.请求分页系统中的置换算法
6.进程通信
信号、管道、消息队列、共享内存:
(以下介绍来自转载:Fei Guo)
多进程:
首先,先来讲一下fork之后,发生了什么事情。
由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。
fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
可以这样想象,2个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因
至于那一个最先运行,可能与操作系统(调度算法)有关,而且这个问题在实际应用中并不重要,如果需要父子进程协同,可以通过原语的办法解决。
常见的通信方式:
1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
4. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
6. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
7. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
8. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
管道:
管道允许在进程之间按先进先出的方式传送数据,是进程间通信的一种常见方式。
管道是Linux 支持的最初Unix IPC形式之一,具有以下特点:
1) 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
2) 匿名管道只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
3) 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
管道分为pipe(无名管道)和fifo(命名管道)两种,除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。
pipe用于相关进程之间的通信,例如父进程和子进程,它通过pipe()系统调用来创建并打开,当最后一个使用它的进程关闭对他的引用时,pipe将自动撤销。
FIFO即命名管道,在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,通过mknode()系统调用或者mkfifo()函数来建立的。一旦建立,任何进程都可以通过文件名将其打开和进行读写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。
无名管道:
pipe的例子:父进程创建管道,并在管道中写入数据,而子进程从管道读出数据
消息队列:
消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。
消息队列的常用函数如下表:
进程间通过消息队列通信,主要是:创建或打开消息队列,添加消息,读取消息和控制消息队列。
例子:用函数msget创建消息队列,调用msgsnd函数,把输入的字符串添加到消息队列中,然后调用msgrcv函数,读取消息队列中的消息并打印输出,最后再调用msgctl函数,删除系统内核中的消息队列。(黄色部分是消息队列相关的关键代码,粉色部分是读取stdin的关键代码)
共享内存:
共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取错做读出,从而实现了进程间的通信。
采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队里等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。
一般而言,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时在重新建立共享内存区域;而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件,因此,采用共享内存的通信方式效率非常高。
共享内存有两种实现方式:1、内存映射 2、共享内存机制
1、内存映射
内存映射 memory map机制使进程之间通过映射同一个普通文件实现共享内存,通过mmap()系统调用实现。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read/write等文件操作函数。
例子:创建子进程,父子进程通过匿名映射实现共享内存。
分析:主程序中先调用mmap映射内存,然后再调用fork函数创建进程。那么在调用fork函数之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap函数的返回地址,这样,父子进程就可以通过映射区域进行通信了。
2、UNIX System V共享内存机制
IPC的共享内存指的是把所有的共享数据放在共享内存区域(IPC shared memory region),任何想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。
和前面的mmap系统调用通过映射一个普通文件实现共享内存不同,UNIX system V共享内存是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。
2、UNIX System V共享内存机制
IPC的共享内存指的是把所有的共享数据放在共享内存区域(IPC shared memory region),任何想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。
和前面的mmap系统调用通过映射一个普通文件实现共享内存不同,UNIX system V共享内存是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。
例子:设计两个程序,通过unix system v共享内存机制,一个程序写入共享区域,另一个程序读取共享区域。
分析:一个程序调用fotk函数产生标准的key,接着调用shmget函数,获取共享内存区域的id,调用shmat函数,映射内存,循环计算年龄,另一个程序读取共享内存。
(fotk函数在消息队列部分已经用过了,根据pathname指定的文件(或目录)名称,以及proj参数指定的数字,ftok函数为IPC对象生成一个唯一性的键值。)
key_t ftok(char* pathname,char proj)