操作系统对于计算机来说是用于管理计算机的硬件或软件资源的管理系统。对用户来说,操作系统为用户提供服务接口。
资源分配,资源回收,为应用程序提供服务,管理应用程序
CPU管理,内存管理,外存管理,IO管理四个方面
进程是计算机动态执行静态代码的一个过程
PCB,代码块,数据块
动态性,并发性,独立性,异步性
创建,就绪态,执行态,阻塞态,结束,另外还有就绪挂起和阻塞挂起状态
(1)硬件压入堆栈程序计数器
(2)硬件从中断向量装入新的程序计数器
(3)汇编语言过程保存寄存器的值
(4)汇编语言设置新的堆栈
(5)C中断服务例程运行
(6)调度程序决定下一个将运行的进程
(7)C过程返回至汇编代码
(8)汇编语言过程开始运行新的当前进程
(1)保存断点
(2)中断服务程序寻址
(3)关中断
(4)保存现场
(5)开中断
(6)执行中断服务程序
(7)关中断
(8)恢复现场
(9)开中断
(10)中断返回
标识信息:内部标识符和外部标识符
处理机状态信息:通用寄存器的内容,控制寄存器的内容,程序状态字PSW,用户栈指针
进程调度信息:进程状态,优先级,调度所需要的其他信息,事件ss
进程控制信息:程序和数据的地址,进程同步和通信机制,资源清单,链接指针
并发是宏观上并行,微观上串行,进程交替使用CPU,给用户一种一起执行的感觉
并行是多个CPU处理多个进程,是真正的同时处理
进程是资源分配的基本单位,线程是调度的基本单位
系统开销不同,进程切换开销比线程切换开销大。同一进程拥有的线程切换只需要维护少量寄存器信息,而进程的切换需要进行页表替换,当发生缺页时还需要从外存中读取信息,io的执行速度比较慢。
并发程度不同,进程进程之间并发执行,而同一进程的线程也可以并发执行,引用线程以后并发程度高了
守护进程是后台执行的一种独立于控制终端的周期执行的进程,包括web服务器,http等都属于控制进程。
创建守护进程:
(1)使用fork()创建子进程,并使父进程退出,使子进程在后台执行
(2)调用setsid()创建一个新的对话期,进程成为一个会话组长和进程组长,使子进程和父进程脱离
(4)获取最高文件描述符值,然后通过1循环把所有描述符关闭。
(5)将当前目录改为根目录
(6)使用unmask(0)将所有的屏蔽字清零
(7)处理SIGCHLD信号,在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,⼦进程结束时不会产⽣僵⼫进程。
创建一个写helloworld的守护进程
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
pid_t p=fork();
if(p<0)
{
perror("fork failed\n");
exit(-1);
}
else if(p==0) //子进程
{
setsid();
chdir("/");
umask(0);
for(int i=0;i<3;i++)
close(i);
int fd=open("/Users/tom/Desktop/data.txt",O_RDWR);
for(int j=0;j<1000;j++)
{
sleep(2); //每隔两秒就像文件中写入
write(fd,"hello wofff",11);
}
}
else //父进程
{
exit(0);
}
return 0;
}
关于fork(),会创建子进程,子进程的返回值是0,父进程的返回值是子进程的pid;
可以通过ps -aux|awk ‘$11~“/path/”{print $2}’|xargs kill -9 进行杀死
子进程结束后,如果父进程没有捕捉到,还认为子进程存在并继续保留子进程的资源,这是子进程就是僵尸进程
设置僵尸进程的目的是维护已经被杀死的子进程的信息,在父进程需要时候使用。
杀死僵尸进程:父进程调用wait/waitpid等函数可以得到子进程信息,并杀死僵尸进程。
wait:父进程等待子进程退出,并造成父进程阻塞
int status = 0;
wait(&status);
if(WIFEXITED(status) != 0)//判断是否正常退出,如果是,就返回一个非零值
{
printf("子进程的返回值是%d\n",WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))
{
printf("子进程被信号%d\n",WTERMSIG(status));//此处检查子进程因何异常终止
}
waitpid:父进程等待pid参数指定的子进程,若目标结束返回子进程pid,否则返回0;
pid_t waitpid (pid_t pid, int* statusp, int options);
返回 :如果成功,则为子进程的PID,如果options为WNOHANG,则返回0,如果发生其他错误,则返回-1。
WNOHANG: 如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。 默认的行为是挂起调用进程,直到有子进程终止 。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。
WUNTRACED: 挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止 。返回的PID 为导致返回的已终止或被停止子进程的 PID。默认的行为是只返回已终止的子进程。当你想要检査已终止和被停止的子进程时,这个选项会有用。
WCONTINUED: 挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中一 个被停止的进程收到 SIGCONT 信号重新开始执行。
不用wait\waitpid去避免产生孤儿进程:使用signal(SIGCHLD, SIG_IGN)
(1) 通过signal(SIGCHLD, SIG_IGN)通知内核对⼦进程的结束不关⼼,由内核回收。如
果不想让⽗进程挂起,可以在⽗进程中加⼊⼀条语句:signal(SIGCHLD,SIG_IGN);
表⽰⽗进程忽略SIGCHLD信号,该信号是⼦进程退出的时候向⽗进程发送的。
(2) 忽略SIGCHLD信号,这常⽤于并发服务器的性能的⼀个技巧因为并发服务器常常
fork很多⼦进程,⼦进程终结之后需要服务器进程去wait清理资源。如果将此信号
的处理⽅式设为忽略,可让内核把僵⼫⼦进程转交给init进程去处理,省去了⼤量
僵⼫进程占⽤系统资源。
进程由代码段,堆栈段,数据段几个部分组成,代码段是静态的二进制代码,多个进程可以共享
(1)父进程创建子进程后,除了pid其他部分都一样
(2)子进程共享父进程的全部数据,并且在写数据时会将公共数据拷贝一份,然后在拷贝的数据上进行操作。
(3)如果子进程拥有自己想要运行的代码段,可以通过调用execv()函数重新加载新的代码段,之后就与父进程分离开了。
(1)先来先服务
(2)最短作业优先
(3)最短剩余时间优先
(4)时间片轮转
(5)优先级调度算法
(6)多级队列
(7)最短进程优先算法
(1)无名管道
(2)有名管道
(3)信号
(4)信号量
(5)消息队列
(6)共享内存
(7)存储映射
(8)socket
(1)管道没有名字,只能用于父子进程或者兄弟进程之间通信。
(2)半双工通信
(3)传输的数据是无格式的
(4)实际上是内存中的一个缓冲区
(5)存在阻塞方式
(6)管道数据只能读一次,即使读不完,也会清空管道。
#include
/**
* 创建⽆名管道.
* @param pipefd 为int型数组的⾸地址,其存放了管道的⽂件描述符
* pipefd[0]、pipefd[1].
* @return 创建成功返回0,创建失败返回-1.
*/
int pipe(int pipefd[2]);
/**
* 当⼀个管道建⽴时,它会创建两个⽂件描述符 fd[0] 和 fd[1]。其中
* fd[0] 固定⽤于读管道,⽽ fd[1] 固定⽤于写管道。
* ⼀般⽂件 I/O的函数都可以⽤来操作管道(lseek() 除外。)
*/
#include
/**
* 该函数可以通过name参数查看不同的属性值.
* @param fd ⽂件描述符.
* @param name _PC_PIPE_BUF,查看管道缓冲区⼤⼩;
* _PC_NAME_MAX,⽂件名字字节数的上限.
* @return 成功: name返回的值的决定name的意义; 失败: 返回-1.
*/
long fpathconf(int fd, int name);
// 获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
// 设置新的flags
flag |= O_NONBLOCK;
// flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
以文件形式存放于内存之中,只要访问到有名管道,就可以进行相互的通信。
通过命令创建有名管道:
(1)mkfifo fifo
(2)eg创建fifo文件
(3)fifo形式文件,存在于内容中,查看文件大小位0
通过函数创建有名管道
#include
#include
/**
* 命名管道的创建.
* @param pathname 普通的路径名,也就是创建后 FIFO 的名字.
* @param mode ⽂件的权限,
* 与打开普通⽂件的 open() 函数中的 mode 参数相同。(0666).
* @return 成功: 0 状态码;
* 失败: 如果⽂件已经存在,则会出错且返回 -1.
*/
int mkfifo(const char *pathname, mode_t mode
(1)读管道
如果管道有数据就读
如果管道没数据,写端关闭,则返回
如果管道没数据,写端没有关闭,则阻塞
(2)写管道
如果读端关闭,则返回异常
如果读端未关闭,管道满了,则阻塞
如果读端未关闭,管道未满,写入数据并返回写入了多少数据
存储映射I/O (Memory-mapped I/O) 使⼀个磁盘⽂件与存储空间中的⼀个缓冲区相映射.
于是当从缓冲区中取数据,就相当于读⽂件中的相应字节。于此类似,将数据存⼊缓冲区,
则相应的字节就⾃动写⼊⽂件。
这样,就可在不适⽤read和write函数的情况下,使⽤地址(指针)完成I/O操作,进程就可
以直接通过读写内存来操作⽂件.
共享内存可以说是最有⽤的进程间通信⽅式,也是最快的IPC形式, 因为进程可以直接读写
内存,⽽不需要任何数据的拷贝.
#include
void *mmap(void *addr, size_t length,
int prot, int flags, int fd, off_t offset);
参数详解:
addr:映射的地址
Length:映射的文件大小(长度)
Prot:映射区的保护方式,(1)读:PROT_READ(2)写:PROT_WRITE(3)读写:PROT_READ|PROT_WRITE
Flags:映射方式(1)MAP_SHARED:写入映射区的内容会直接写入文件(2)MAP_PRIVATE:写入映射区的文件会写入副本,而不会直接写入文件
Fd:映射的文件
Offset:4k的整数倍,通常为0
作用:创建共享存储区并实现映射
#include
/**
* 释放内存映射区.
* @param addr 使⽤mmap函数创建的映射区的⾸地址.
* @param length 映射区的⼤⼩.
* @return 成功返回0; 失败返回-1.
*/
int munmap(void *addr, size_t length);
参数详解:共享存储区首地址,长度
作用:取消共享存储区
(1)为什么使⽤匿名的⽅式实现通信?
内存映射的需要依赖⽂件。⽽建⽴⽂件建⽴好了只会还要unlink close掉,⽐较⿇烦;
(2)有什么好的不能办法进⾏解决?
直接使⽤匿名映射来代替;
(3)Linux系统给我们提供了创建匿名映射区的⽅法,⽆需依赖⼀个⽂件即可创建映射
区。同样需要借助标志位参数flags来指定;
(4)使⽤MAP_ANONYMOUS (或MAP_ANON):
保存在内核中的消息链表
13.信号
1-31号为常规信号(也叫普通信号或者标准信号)
34-64称之为实时信号,驱动编程和硬件相关。名字区别不大,而前32个名字各不相同
SIGINT 当⽤户按下了
的程序发出此信号,终⽌进程
SIGQUIT ⽤户按下
端启动的程序发出些信号,终⽌进程
SIGSEGV 指⽰进程进⾏了⽆效内存访问(段错误), 终⽌进程并产⽣core⽂件
SIGPIPE Broken pipe向⼀个没有读端的管道写数据,终⽌进程
SIGCHLD ⼦进程结束时,⽗进程会收到这个信号,忽略这个信号
(1)编号
(2)名称
(3)事件
(4)默认处理动作
SIGKILL和SIGSTOP信号,不允许忽略和捕捉,只能执⾏默认动作。甚⾄不能将其设置为阻塞.
(1)产生
(2)未决状态:没有被处理
(3)递达状态
(1)阻塞信号集
将某些信号加⼊集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将
推后(处理发⽣在解除屏蔽后)
(2)未决信号集合
信号产⽣,未决信号集中描述该信号的位⽴刻翻转为1,表⽰信号处于未决状态。当信号被
处理对应位翻转回为0。这⼀时刻往往⾮常短暂。
(1)kill函数
int kill(pid_t,int sig)
sig是信号状态
(2)raise函数
int raise(int sig)
自己给自己发送指定信号
(3)abort函数
void abort(void);
自己给自己发送终止信号
(4)alarm函数(闹钟)
#include
/**
* 设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程
收到该信号,默认动作终⽌。每个进程都有且只有唯⼀的⼀个定时器;
* 取消定时器alarm(0),返回旧闹钟余下秒数.
* @param seconds 指定的时间,以秒为单位.
* @return 返回0或剩余的秒数.
* /
unsigned int alarm(unsigned int seconds);
(5)setitimer函数(定时器)
#include
struct itimerval {
struct timerval it_interval; // 闹钟触发周期
struct timerval it_value; // 闹钟触发时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
}
/**
* 设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时.
* @param which 指定定时⽅式:
* (1) ⾃然定时:ITIMER_REAL → 14)SIGALRM计算⾃然时间;
* (2) 虚拟空间计时(⽤户空间):ITIMER_VIRTUAL → 26)
SIGVTALRM 只计算进程占⽤cpu的时间;
* (3) 虚拟空间计时(⽤户空间):ITIMER_VIRTUAL → 26)
SIGVTALRM 只计算进程占⽤cpu的时间.
* @param new_value 负责设定timeout时间.
* @param old_value 存放旧的timeout值,⼀般指定为NULL.
* @return 成功: 0; 失败: -1.
*/
int setitimer(int which, const struct itimerval *new_value, struct
itimerval *old_value);
// itimerval.it_value: 设定第⼀次执⾏function所延迟的秒数
// itimerval.it_interval: 设定以后每⼏秒执⾏function
(1)不可重入函数
函数体内使用了静态的数据结构
函数体内调用了malloc或者free函数
函数体内调用了标准io函数
(2)可重入函数
写函数时候尽量选择局部变量
对于全局函数或者静态变量,会加信号量进行互斥访问
对于不可重入函数,调用其时可能会修改其他任务调用这个函数的数据。这样的话就造成的数据紊乱,所以限制其不可重入。
(1)进程终止时
(2)子进程接收到SIGSTOP信号终止时
(3)进程处于停止状态,节点后SIGCINT后唤醒时。
(1)使用wait和waitpid等函数,由父进程处理
(2)如果父进程很忙可以通过signal()函数人为处理信号
(3)如果父进程不关心子进程,可以用signal(SIGCHLD,SIG_IGN)通知内核,由内核进行管理
(1)会话中有多个进程组,创建该会话的进程是进程组组长
(2)会话的进程组组长不能创建新的会话
(3)会话中的子进程可以创建新的会话,创建完后从属于新的会话,创建会话函数是sid=setsid();既将子进程从原来会话中独立出去
(4)新的会话会放弃原有的控制终端,该会话没有控制终端,但自己可以申请控制终端
(1)getsid函数
获取进程所属的会话sid
#include
pid_t getsid(pid_t pid)
通过进程pid获取会话的sid
(2)setsid函数
创建一个会话
实际上线程是轻量级的进程,线程拥有自己的PCB,拥有各种寄存器,以及程序计数器指针等私有空间,但数据空间是和其他线程进行共享的。线程和进程的调用都是使用clone函数,当设置为共享数据区时是线程,不共享时是进程,
(1)用户级线程
(2)内核级线程
(3)轻量级线程LWP:内核支持的用户线程,像普通进程一样被调度。每个轻量级线程都需要一个内核线程支持,而用户线程是工作在LWP上面的。
(1)文件描述符
(2)各种信号的处理方式
(3)当前工作目录
(4)用户ID和组ID
(1)线程id
(2)处理器现场以及栈指针
(3)独立的栈空间
(4)errno变量
(5)信号屏蔽字
(6)调度优先级
(1)优点
并发程度高
开销小:创建快,终止快,切换快,通信快
数据通信和资源共享更方便
(2)缺点
库函数,不稳定
调试、编写困难、gdb不支持
对信号支持不好
进程号用pid_t表示,线程号用pthread_t表示,linux用无符号整数表示。
(1)pthread_self()函数:返回线程的调用者
(2)pthread_equal(pthread_t t1,pthread_t t2)函数:判断两个线程号是否相等
(3)pthread_create函数
#include
/**
* 创建⼀个线程.
* @param thread 线程标识符地址.
* @param attr 线程属性结构体地址,通常设置为 NULL.
* @param start_routine 线程函数的⼊⼝地址.
* @param arg 传给线程函数的参数.
* @return 成功: 0; 失败: ⾮0.
*/
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg );
// 由于pthread_create的错误码不保存在errno中,
// 因此不能直接⽤perror()打印错误信息,
// 可以先⽤strerror()把错误码转换成错误信息再打印
(4)pthread_join(ptread_t thread,void **retval)函数:线程资源回收函数,thread是线程号,retval是存储线程返回时候的信息。
(5)pthread_detach(ptread_t thread)函数:将线程与进程分离,线程与进程分离后,进程将不再负责线程的结束,而是由进程进行管理
(6)thread_exit(void *retval)函数:线程退出函数,资源不会被释放
(7)thread_cancel(pthread_t thread)函数:取消线程,等线程满足取消条件时取消线程
(8)线程栈地址
如果进程的线程栈地址不够,会调用malloc开辟新的空间,分配新的空间
可以自己设置线程栈的地址,也可以得到线程栈的地址
#include
/**
* 设置线程的栈地址.
* @param attr 指向⼀个线程属性的指针.
* @param stackaddr 内存⾸地址.
* @param stacksize 返回线程的堆栈⼤⼩.
* @return 成功: 0; 失败: 错误号.
*/
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr,
size_t stacksize);
/**
* 获取线程的栈地址.
* @param attr 指向⼀个线程属性的指针.
* @param stackaddr 返回获取的栈地址.
* @param stacksize 返回获取的栈⼤⼩.
* @return 成功: 0; 失败: 错误号.
*/
int pthread_attr_getstack(const pthread_attr_t *attr, void
**stackaddr, size_t *stacksize);
可以设置线程栈的大小,也可以得到线程栈的大小
#include
/**
* 设置线程的栈⼤⼩.
* @param attr 指向⼀个线程属性的指针.
* @param stacksize 线程的堆栈⼤⼩.
* @return 成功: 0; 失败: 错误号.
*/
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t
stacksize);
/**
* 获取线程的栈⼤⼩.
* @param attr 指向⼀个线程属性的指针.
* @param stacksize 返回线程的堆栈⼤⼩.
* @return 成功: 0; 失败: 错误号.
*/
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t
结束了但没有回收的线程,线程结束的方式有:
(1)pthread_join
(2)pthread_detach
(3)pthread_create
(4)malloc和mmap申请的线程可以被其他线程释放
一个进程内部有多个线程并发执行
进程同步是由于进程相互协作而形成的制约关系
进程互斥是由于多个进程竞争临界资源而形成的制约关系
软件方法、硬件方法、信号量方法、管程、消息传递
(1)创建信号量
#include
•/**
* 创建⼀个信号量并初始化它的值。⼀个⽆名信号量在被使⽤前必须先初始化.
* @param sem 信号量的地址.
* @param pshared 等于 0,信号量在线程间共享(常⽤); 不等于0,信号量在进程间共
享.
* @param value 信号量的初始值.
* @return 成功: 0; 失败: -1.
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
(2)删除信号量
#include
•/**
* 删除 sem 标识的信号量.
* @return 成功: 0; 失败: -1.
*/
int sem_destroy(sem_t *sem);
(3)P操作
#include
•/**
* 将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0;
* 若信号量为 0,此函数会阻塞,直到信号量⼤于 0 时才进⾏减 1 操作。
* @param sem 信号量的地址.
* @return 成功: 0; 失败: -1.
*/
int sem_wait(sem_t *sem);
/**
* 以⾮阻塞的⽅式来对信号量进⾏减 1 操作。
* 若操作前,信号量的值等于 0,则对信号量的操作失败,函数⽴即返回。
*/
int sem_trywait(sem_t *sem);
/**
* 限时尝试将信号量的值减 1
* abs_timeout:绝对时间
*/•
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
(4)V操作
#include
/**
* 将信号量的值加 1 并发出信号唤醒等待线程(sem_wait()).
* @param sem 信号量的地址.
* @return 成功: 0; 失败: -1.
*/
int sem_post(sem_t *sem);
(5)获取信号量的值
#include
•/**
* 获取 sem 标识的信号量的值,保存在 sval 中.
* @param sem 信号量的地址.
* @param sval 保存信号量值的地址.
* @return 成功: 0; 失败: -1.
*/
int sem_getvalue(sem_t *sem, int *sval);\
(1)创建互斥锁pthread_mutex_init()
#include
•/**
* 初始化⼀个互斥锁.
* @param mutex 互斥锁地址。类型是 pthread_mutex_t.
* @param attr 设置互斥量的属性,通常可采⽤默认属性,即可将 attr 设为 NULL.
* @return 成功: 0 成功申请的锁默认是打开的; 失败: ⾮0(错误码).
*/
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 这种⽅法等价于使⽤ NULL 指定的 attr 参数调⽤ pthread_mutex_init() 来完成
动态初始化,
// 不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进⾏错误检查。
(2)销毁互斥锁pthread_mutex_destroy()
#include
••/**
* 销毁指定的⼀个互斥锁。互斥锁在使⽤完毕后,必须要对互斥锁进⾏销毁,以释放资源.
* @param mutex 互斥锁地址。类型是 pthread_mutex_t.
* @return 成功: 0; 失败: ⾮0(错误码).
*/
int pthread_mutex_destroy(pthread_mutex_t *mutex);
(3)互斥锁上锁pthread_mutex_lock()
#include
•/**
* 对互斥锁上锁,若互斥锁已经上锁,则调⽤者阻塞,直到互斥锁解锁后再上锁.
* @param mutex 互斥锁地址.
* @return 成功: 0; 失败: ⾮0(错误码).
*/
int pthread_mutex_lock(pthread_mutex_t *mutex);
/**
* 调⽤该函数时,若互斥锁未加锁,则上锁,返回 0;
* 若互斥锁已加锁,则函数直接返回失败,即 EBUSY.
*/
int pthread_mutex_trylock(pthread_mutex_t *mutex);
(4)互斥锁解锁pthread_mutex_unlock()
#include
•/**
* 对指定的互斥锁解锁.
* @param mutex 互斥锁地址.
* @return 成功: 0; 失败: ⾮0(错误码)
*/
int pthread_mutex_unlock(pthread_mutex_t *mutex);
进程互相占有资源又等待资源而造成的一种集体等待现象,如果没有外界干预,这种状态将会一直执行下去
(1)互斥
(2)占有和等待
(3)不可抢占
(4)环路等待
(1)预防死锁:破坏死锁产生条件
(2)避免死锁:银行家算法
(3)检测和处理死锁
(1)生产者消费者问题
(2)读者写者问题
(3)哲学家进餐问题
页面:程序地址空间被划分成若干固定大小的片断
页框:物理内存被划分成相同大小的块
机制:把计算机内存分成大小相同的块,程序分散的分布在内存当中,用页表存储程序分布在哪个物理块,建立逻辑地址和物理地址的对应关系。
程序先通过逻辑地址和页表查询页表,找到实际物理块号,再加上页内偏移量得到实际物理地址。
分段机制
以段为基本分配单位的存储管理方式。
分段存储管理的优点:
(1)便于程序模块化设计
(2)便于动态链接
(3)便于保护和共享
(4)无内部碎片
分段存储管理的缺点:
(1)地址转换需要硬件的支持——段表寄存器
(2)分段的最大尺寸受到主存可用空间的限制
(3)有外部碎片
(1)页是信息的物理单位,分页的目的是实现离散分配,减少内存的外部碎片,提高内存的利用率。或者说,分页仅仅是由于系统管理的需要而不是用户的需要。段则是信息的逻辑单位,它含有一组意义相对完整的信息。分段的目的是为了能更好地满足用户的需要。
(2)页的大小固定且由系统决定,由系统把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的,因而在系统中只能有一种大小的页面;而段的长度却不固定,决定于用户所编写的程序,通常由编译程序在对源程序进行编译时,根据信息的性质来划分。
(3)分页的作业地址空间是一维的,即单一的线性地址空间,程序员只需利用一个记忆符,即可表示一个地址;而分段的作业地址空间则是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。
(4)分页存储管理系统不易实现“共享”和“运行时动态链接”,而分段系统易于实现实现。
把外存空间编入逻辑空间,进程可部分保存在内存里面,页表记录对应逻辑页面在外存的位置以及记录是否在内存里面。
当访问时,先判断页面是否在内存,不在的话从外存调入内存,若内存分配的页表已经满了,会执行页面置换算法,将一些页面调出。
分页算法
(1)最佳页面置换算法
(2)先进先出置换算法
(3)最近最久未使用置换算法:可能出现抖动现象
(4)时钟页面置换算法
(5)改进的时钟页面置换算法
虚拟存储系统需要的硬件支持
(1)相当数量的外存一定容量的内存
(2)请求分页或分段的页表或段表机制
(3)缺页或缺段机构地址变换机构
以分页为例
(1)虚拟内存可以结合磁盘和物理内存的优势为进程提供看起来速度⾜够快并且容量⾜够⼤的存储。
(2)虚拟内存可以为进程提供独⽴的内存空间并引⼊多层的页表结构将虚拟内存翻译成物理内存,进程之间可以共享物理内存减少开销,也能简化程序的链接、装载以及内存分配过程。
(3)虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提⾼系统的安全性。
快表的工作原理类似于系统中的数据高速缓存(cache),其中专门保存当前进程最近访问过的一组页表项。根据局部性原理,在一个很短的时间段内,程序的执行总是局部于某一个范围。即,进程最近访问过的页面在不久的将来还可能被访问。
(1)create
(2)delete
(3)open
(4)close
(5)read
(6)write
(7)append
(8)seek
(9)get attributes
(10)set attributes
(11)rename
(1)先进先出
(2)最短寻道时间算法
(3)扫描算法
(1)CPU向外设控制器发出指令,既out指令
(2)形成文件视图(为了统一out指令的形式)
(3)中断
库函数调用(printf)->调用系统接口(write)->系统调用字符设备接口(crw_table)->tty设备写(tty——write)->显示器写(con_write)