操作系统知识

同步I/O操作:导致请求进程阻塞,直到操作完成。

异步I/O操作:不导致请求进程阻塞。(会立即返回,以通知方式告知)

并发与并行

并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。

并发是一段时间内处理很多事情,并行是某一时刻同时做很多事情。

进程是具有独立功能的程序,关于某个数据集合的一次运行过程。

进程与程序的区别:

1.    程序是静态的(存在是永久的),进程是动态的(具有生命周期)

2.    进程=程序+数据+PCB(进程控制块-Process Control Block,一种数据结构)

3.    一个程序可以对应多个进程(例:word程序可以实现多种功能)

4.    一个进程可以包含多个程序(例:将不同的C语言,汇编语言一起连接调度使用) 

注:PCB是有结构的主存区,包含进程执行的所有信息,系统通过PCB来感知进程的各个状态,是进程存在的唯一标志。操作系统维护着一张表格(一个结构数组),即进程表,每个进程占用一个进程表项。

进程表包括:

1.    程序计数器(PC),程序状态字(PSW),寄存器

2.    栈(用于保存临时信息),堆(保存动态的数据信息) 

3.    数据段,文本段(如代码)

4.    文件描述符

操作系统维护着一张表格(一个结构数组),即进程表。每个进程占用一个进程表项。下图为进程表项中的一些字段:

 每个进程在PCB中都保存着一份文件描述符表(指针数组),文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。

具体可参考:https://blog.csdn.net/u012877472/article/details/49923811

一、 线程与进程

 进程是操作系统资源分配的基本单位,线程是cpu调度的基本单位。

 进程实现是对运行程序的封装,实现了操作系统的并发;线程是进程的子任务,用于保证程序的实时性,实现了进程内部的并发。

 进程在运行过程中具有独立的内存单元(地址空间),而多个线程可以共享所属进程的内存(共享:代码段,全局变量,堆,文件描述符等),但每个线程有自 己的线程上下文,线程ID,栈,程序计数器等。

二、 进程的三种基本状态 

  • 就绪状态 

进程已分配到除CPU以外的所有资源,但是没有得到处理机 

  • 执行状态 

进程已获得处理机,程序正在处理机上执行 

  • 阻塞状态 

进程在处理机上执行的过程中,遇到了等待某事物无法进行,便放弃处理机处于阻塞状态。

 

三、 线程与进程同步的方式

1. 进程的同步方式

进程同步是指多个相关进程在执行次序上的协调以达到互斥地使用临界区资源。

进程间同步的主要方法有原子操作、信号量机制、自旋锁、管程、会合、分布式系统等。

2. 线程的同步方式

临界区:通过对多线程的串行化来访问公共资源或者一段代码,速度快,适合控制数据访问,最多一个线程可以访问临界区资源。其他线程要访问必须先被挂起,然后等待该进程的离开,才能继续访问。

互斥量:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。

信号量:它是一个计数器,允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。(PV操作都是原子操作,占用一个资源P-1,释放一个资源V+1)

用信号量来解决生产者和消费者问题,定义3个信号量,一个为full(充满缓冲槽的数目,初始为0),一个empty(空缓冲槽的数目,初始为槽的大小),一个为mutex初值为1,保证同时只要一个进程可以进入临界区。

区别:互斥量是简化版的信号量,不需要信号量的计数能力,但互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。

总而言之:临界区是一种轻量级的同步机制,与互斥和事件这些内核同步对象相比,临界区是用户态下的对象,即只能在同一进程中实现线程互斥。因无需在用户态和核心态之间切换,所以工作效率比较互斥来说要高很多。

互斥量管理的是资源的使用权,而信号量管理的是资源的数量

线程同步方式:互斥量,信号量,条件变量,读写锁,自旋锁,屏障

锁可以分为递归锁和非递归锁。 递归锁也叫可重入锁,非递归锁也叫不可重入锁。 

二者唯一的区别是: 

同一个线程可以多次获取同一个递归锁,不会产生死锁。 如果一个线程多次获取同一个非递归锁,则会产生死锁。

例如读锁是递归锁,而写锁是非递归锁。

通常,递归锁是在非递归互斥锁加引用计数器来实现的。简单的说,在加锁前,先判断上一个加锁的线程和当前加锁的线程是否为同一个。如果是同一个线程,则仅仅引用计数器加1。如果不是的话,则引用计数器设为1,则记录当前线程号,并加锁。

线程函数的返回值:成功为0,失败为错误编号(大部分)

互斥量:

pthread_mutex_t mutex;
pthread_mutex_init(&mutex,NULL);

void pthread1(void* arg){
   pthread_mutex_lock(&mutex);
   .....//临界区
   pthread_mutex_unlock(&mutex);
}

void pthread2(void* arg){
   pthread_mutex_lock(&mutex);
   .....//临界区
   pthread_mutex_unlock(&mutex);
}

信号量:

pthread_mutex_t mutex;
sem_t full=0,empty=N;

void producer(void* arg){
    while(1){
    sem_wait(&empty);       //empty P操作 减一
    pthread_mutex_lock(&mutex);
    ...//produce a resource
    pthread_mutex_unlock(&mutex);
    sem_post(&full);         //full V操作 加一
    }
}
void consumer(void* arg){
    while(1){
    sem_wait(&full);
    pthread_mutex_lock(&mutex);
    ...//consume a resource
    pthread_mutex_unlock(&mutex);
    sem_post(&empty); 
    }
}

条件变量:

     互斥量是用于上锁,条件变量用于等待;(条件变量的使用一定要结合互斥锁)

     为什么条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的呢?因为“某个特性条件”通常是在多个线程之间共享的某个变量。互斥锁允许这个变量可以在不同的线程中设置和检测。

    条件变量是在多线程程序中用来实现“等待->唤醒”逻辑常用的方法。例如,应用程序A中包含两个线程t1和t2。t1需要在test_cond为true时才能继续执行,而test_cond的值是由t2来改变。

    例如:t1在test_cond为false时调用cond_wait进行等待,t2在改变test_cond的值后,调用cond_signal,唤醒在等待中的t1,告诉t1 test_cond的值变了,这样t1便可继续往下执行。具体如下:

pthread_mutex_t mutex;  ///< 互斥锁
pthread_cond_t  cond;   ///< 条件变量
bool test_cond = false;
/// TODO 初始化mutex和cond
 
/// thread 1:
pthread_mutex_lock(&mutex);            ///< 1
while (!test_cond)
{
    pthread_cond_wait(&cond, &mutex);  ///< 2,3
}
pthread_mutex_unlock(&mutex);          ///< 4
RunThread1Func();
 
/// thread 2:
pthread_mutex_lock(&mutex);            ///< 5
test_cond = true;
pthread_mutex_unlock(&mutex);          ///< 6
pthread_cond_signal(&cond);            ///<  7

/// TODO 销毁mutex和cond

在这里1、4、5、6都是正常的lock/unlock,2、3是需要特别说明的。2是进入pthread_cond_wait后的,pthread_cond_wait调的pthread_mutex_unlock,这样做的目的是为了保证在thread1阻塞wait后,thread2获取同一把锁mutex的时候,能够正常获取(即5,6)。3是thread1被唤醒后,要退出pthead_cond_wait之前,pthread_cond_wait调的pthread_mutex_lock,这样做的目的是为了把mutex的控制权还给调用pthread_cond_wait的线程(即thread1)。整理一下基本的时序为:

thread 1 lock->thread 1 wait-> thread 1 unlock(in wait)
->thread 2 lock->thread 2 signal->thread 2 unlock
->thread 1 lock (in wait)->thread 1 unlock

   注意:执行pthread_cond_wait(&cond, &mutex);需要占用互斥量(加锁,放在1,4之间),但是执行pthread_cond_signal(&cond);可以不需要占用互斥量,即上面所示可以将发送信号这句放在6之后;

    pthread_cond_signal()函数只会唤醒等待该条件的某个线程,pthread_cond_broadcast()会广播条件状态的改变,以唤醒等待该条件的所有线程。例如多个线程只读共享资源,这可以将它们都唤醒。pthread_cond_wait()和pthread_cond_timewait()成功返回时,线程需要重新计算条件,因为其他线程可能在运行过程中已经改变条件。这是上面为什么用while循环判断的原因。

读写锁:

     读写锁与互斥量类似,但它允许更高的并行性。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

自旋锁:

     自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。 互斥量阻塞线程的方式是使其进入睡眠,而自旋锁是让线程忙等,即不会使其睡眠,而是不断循判断自旋锁已经被解锁。适用于占用自旋锁时间比较短的情况。(这是因为线程不希望在重新调度上花太多的成本)。

     例如:我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和core2上运行,两个线程之间共享着一个资源。自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。

      注意:自旋锁适合于短时间的的轻量级的加锁机制。

屏障:

     屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。用计数的方式来判断是否所有点都达到一致状态。例如pthread_join函数就是一种屏障,允许一个线程等待直到另一个线程退出。还有用8个线程来来排序800万个数,每个线程分别用堆排序100万个数,在主线程对这些结果合并。

条件变量:http://www.wuzesheng.com/?p=1668

                 https://blog.csdn.net/anonymalias/article/details/9174481

面试题例子:https://www.cnblogs.com/skynet/archive/2010/10/30/1865267.html

好文:https://blog.csdn.net/kxcfzyk/article/details/29240953

参考链接:https://www.cnblogs.com/LUO77/p/5754633.html

四、 进程间的通讯方式

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享内存、Socket等。其中 Socket支持不同主机上的两个进程IPC。

父子进程之间不能使用全局变量通信!!!

(1)管道(无名管道)

它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。它可以看成是一种特殊的文件(伪文件),对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中(内核)。它是先进先出的。

(2)FIFO(命名管道)

它也是半双工的,有名管道不同于无名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。其他与无名管道类似。虽然FIFO文件的inode节点在磁盘上,但仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和无名管道相同。

用途:1.FIFO可以用于复制一系列shell命令中的输出流。这就防止了将数据写向中间磁盘文件。这类似于使用管道(|)来避免中间磁盘文件。2.使用FIFO进行客户进程-服务器进程通信

(3)消息队列

消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。它是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除,因为它是以文件的方式建立的。 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。注意第一次读的时候,要考虑上一次没有读完数据的问题

(4)共享内存

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与信号量配合使用,来实现进程间的同步和通信。

(5)套接字

Socket用于描述IP地址和端口,是一个通信链的句柄。(可用于不同主机之间的通讯)。

流式Socket(STREAM):面向连接的Socekt,针对面向连接的TCP服务应用,安全,但是效率低;

数据报式Socket(DATAGAM):无连接的Socket,UDP服务应用。不安全(丢失,顺序混乱,在接受端要分析重排及要求重发),但效率高。

(6)信号

信号比较简单,携带信息量少,用于通知接收进程某个事件已经发生。

系统调用kill是用来发送信号给指定进程的;  kill(getpid(),SIGKILL);

系统调用signal是进程用来设定某个信号的处理方法;signal(SIGINT,myfunc)//注册信号处理函数

内核实现信号捕捉的过程如下所示:

使用信号进行进程通讯:1.双方约定信号 ;2.双方注册信号捕捉函数 ;3.编写对应的信号处理函数

未决信号集:已产生,但没有被当前进程处理的信号。

五、线程间的通讯方式

锁(互斥锁,读写锁,条件变量)信号量机制信号机制 
线程之间通信主要是为了线程同步,因为它们一般都共享进程内的数据,所以线程没有进程那种用于交换的机制

六、死锁

死锁是多个进程因为争夺资源而产生互相等待,若无外力,进程无法推进的情况  

死锁产生的三个原因是:(1)系统资源不足(2)系统资源分配不合理(3)进程推进顺序不合理  

死锁的必要条件(缺一不可):

  • 互斥(唯一)条件:一个资源一次只能被一个进程使用
  • 占有和等待条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
  • 不可剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系

死锁的处理策略:鸵鸟策略(忽略)、预防策略(破坏四个必要条件之一)、避免策略(对资源进行分配,动态避免)、检测与恢复策略

死锁的预防:如破坏“占有和等待”(采用一次性将资源全部分配给某个进程);如破坏“不可剥夺”(允许进程剥夺其他进程的资源);如破坏“循环等待”(采用资源有序分配法,对资源进行编号,每个进程必须从小到大依次获得资源)。 

死锁的避免:银行家算法。基本思想是动态地检测资源分配状态,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是,如果存在一个安全序列,那么系统处于安全状态。

死锁解除:常用两种方法为进程终止和资源抢占。所谓进程终止是指简单地终止一个或多个进程以打破循环等待,包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;所谓资源抢占是指从一个或多个死锁进程那里抢占一个或多个资源,此时必须考虑三个问题:

  (I). 选择一个牺牲品 
  (II). 回滚:回滚到安全状态 
  (III). 饥饿(在代价因素中加上回滚次数,回滚的越多则越不可能继续被作为牺牲品,避免一个进程总是被回滚)

 活锁:  活锁指任务和执行者没有被阻塞,但是不断调用,并因某些因素而调用失败,一直重复失败中。

七、 操作系统中进程调度策略有哪几种?

  • FCFS(先来先服务,队列实现,非抢占的):先请求CPU的进程先分配到CPU

  • SJF(最短作业优先调度算法):平均等待时间最短,但难以知道下一个CPU区间长度

  • 优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;解决方案:老化

  • 时间片轮转调度算法(可抢占的):队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。

  • 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。

  • 多级反馈队列调度算法:与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。

八、分页和分段有什么区别(内存管理)?

  段式存储管理是一种符合用户视角的内存分配管理方案。在段式存储管理中,将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)

  页式存储管理方案是一种用户视角内存与物理内存相分离的内存分配管理方案。在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的帧,程序加载时,可以将任意一页放入内存中任意一个帧,这些帧不必连续,从而实现了离散分离。页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满)。

两者的不同点:

  • 目的不同:分页是由于系统管理的需要而不是用户的需要,它是信息的物理单位;分段的目的是为了能更好地满足用户的需要,它是信息的逻辑单位,它含有一组其意义相对完整的信息;

  • 大小不同:页的大小固定且由系统决定,而段的长度却不固定,由其所完成的功能决定;

  • 地址空间不同: 段向用户提供二维地址空间;页向用户提供的是一维地址空间;

  • 信息共享:段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制;

  • 内存碎片:页式存储管理的优点是没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满);而段式管理的优点是没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)。

解决碎片的方法是采取紧凑技术,即对碎片进行拼接,但是需要消耗系统资源。

九、动态分区的分配策略,考虑以下几种算法: 

》首次适应(First Fit)算法:空闲分区以地址递增的次序链接。分配内存时顺序查找,找到大小能满足要求的第一个空闲分区。 

》最佳适应(Best Fit)算法:空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区。 

》最坏适应(Worst Fit)算法:又称最大适应(Largest Fit)算法,空闲分区以容量递减的次序链接。找到第一个能满足要求的空闲分区,也就是挑选出最大的分区。 

》邻近适应(Next Fit)算法:又称循环首次适应算法,由首次适应算法演变而成。不同之处是分配内存时从上次查找结束的位置开始继续查找。

 

十、什么是虚拟内存?

1).内存的发展历程

  没有内存抽象(单进程,除去操作系统所用的内存之外,全部给用户程序使用) —> 有内存抽象(多进程,进程独立的地址空间,交换技术(内存大小不可能容纳下所有并发执行的进程) 
)—> 连续内存分配(固定大小分区(多道程序的程度受限),可变分区(首次适应,最佳适应,最差适应),碎片) —> 不连续内存分配(分段,分页,段页式,虚拟内存)


2).虚拟内存

  虚拟内存允许执行进程不必完全在内存中。虚拟内存的基本思想是:每个进程拥有独立的地址空间,这个空间被分为大小相等的多个块,称为页(Page),每个页都是一段连续的地址。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻进行必要的映射;当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的命令。这样,对于进程而言,逻辑上似乎有很大的内存空间,实际上其中一部分对应物理内存上的一块(称为帧,通常页和帧大小相等),还有一些没加载在内存中的对应在硬盘上,如图5所示。 
注意,请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。

                   这里写图片描述

  由图5可以看出,虚拟内存实际上可以比物理内存大。当访问虚拟内存时,会访问MMU(内存管理单元)去匹配对应的物理地址(比如图5的0,1,2)。如果虚拟内存的页并不存在于物理内存中(如图5的3,4),会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。


3). 页面置换算法

最优置换算法

最优(Optimal, OPT)置换算法所选择的被淘汰页面将是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知未来,因而该算法无法实现。
但最佳置换算法可以用来评价其他算法。使用缺页中断率:

f = F / A(其中F为作业失败访问的次数,A为作业总的访问次数)

先进先出算法(FIFO算法)

淘汰在内存驻留时间最长的页面。只需把调入内存的页面根据先后次序链接成队列,设置一个指针总指向最早的页面。但该算法与进程实际运行时的规律不适应,因为在进程中,有的页面经常被访问。

最近最久未使用算法(LRU算法)

最近最久未使用算法(Least Recently Used),选择最近最长时间未访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的将来可能也不会被访问。该算法为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰。

LRU性能较好,但需要寄存器和栈的硬件支持。LRU是堆栈类的算法,实现起来比较困难,且开销大。试图用比较小的开销接近LRU的性能,这类算法都是时钟置换算法的变体。

LRU实现:双链表+哈希表

HashMap 存储 key,这样可以做到 save 和 get key的时间都是 O(1),而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点;LRU 存储是基于双向链表实现的,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头

参考链接:https://blog.csdn.net/hopeztm/article/details/79547052

最不经常使用淘汰算法(LFU算法)

最不经常使用淘汰算法(Least FrequentlyUsed),淘汰一段时间内,访问次数最少的页面。


4). 虚拟内存的应用与优点

  虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。虚拟内存的使用可以带来以下好处:

  • 在内存中可以保留多个进程,系统并发度提高

  • 解除了用户与内存之间的紧密约束,进程可以比内存的全部空间还大

十一、颠簸

  颠簸本质上是指频繁的页调度行为,具体来讲,进程发生缺页中断,这时,必须置换某一页。然而,其他所有的页都在使用,它置换一个页,但又立刻再次需要这个页。因此,会不断产生缺页中断,导致整个系统的效率急剧下降,这种现象称为颠簸(抖动)。

  内存颠簸的解决策略包括:

  • 如果是因为页面替换策略失误,可以修改替换算法来解决这个问题;

  • 如果是因为运行的程序太多,造成程序无法同时将所有频繁访问的页面调入内存,则要降低多道程序的数量;

  • 否则,还剩下两个办法:终止该进程或增加物理内存容量

十二、局部性原理

(1). 时间上的局部性:最近被访问的页在不久的将来还会被访问;

(2). 空间上的局部性:内存中被访问的页周围的页也很可能被访问。

参考链接:https://blog.csdn.net/justloveyou_/article/details/78304294

http://www.cnblogs.com/memewry/archive/2012/08/25/2656966.html

补充:  

fork()函数的理解 

每个进程都有一个非负整数表示的唯一进程ID,如调度进程ID为0,init进程ID为1,页守护进程ID为2; 

#include<unistd.h> 
#include <sys/types.h> 

main () 

        pid_t pid; 
        pid=fork(); 

        if (pid <0) 
               printf("error in fork!"); 
        else if (pid ==0) 
               printf("i am the child process, my process id is%d\n",getpid()); 
        else 
               printf("i am the parent process, my process id is%d\n",getpid()); 

结果是 
[root@localhost c]# ./a.out 
i am the child process, my process id is4286 
i am the parent process, my process id is4285 

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
    1)在父进程中,fork返回新创建子进程的进程ID;
    2)在子进程中,fork返回0;
    3)如果出现错误,fork返回一个负值;

所以看到的两行输出是来自两个进程,这两个进程来自同一个程序的两次执行。

fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程(新的是子进程,原来是父进程),也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。其实所有进程都是init进程直接或者间接的fork出来的. 

所以子进程是父进程的副本,获得了父进程数据空间、堆和栈的副本;父子进程并不共享这些存储空间,但共享正文段(即代码段);因此子进程对变量的所做的改变并不会影响父进程。

目前采用了写时复制(Copy-On-Write,COW)技术(即fork之后,子进程不会立马执行exec)负责管理父子进程的存储空间关系。数据段、堆栈这些区域是父进程和子进程共享的,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

fork与vfork的区别
1. fork要拷贝父进程的数据段;而vfork则不需要完全拷贝父进程的数据段,在子进程没有调用exec和exit之前,子进程与父进程共享数据段
2. fork不对父子进程的执行次序进行任何限制;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制

僵尸进程没有被父进程处理的后果 
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号,退出状态,运行时间等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程:一个进程使用fork创建子进程,如果子进程退出(exit),而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。因为任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。(即一个终止了但还没有被回收的进程。)

守护进程:是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。 (下图中第一列为必须操作,第二列为建议操作)

                                           

参考网址:https://blog.csdn.net/woxiaohahaa/article/details/53487602

wait:用wait()来等待一个子进程终止运行称为回收进程。wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.

当一个进程正常终止时,内核会向其父进程发送SIGCHLD信号,从而让wait函数执行并返回。

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程, wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

总而言之wait的作用是阻塞父进程自己,直到任意一个子进程结束,同时获得子进程的返回值。

waitpid:比wait更加灵活

1) 可等待特定的进程

2) 可不阻塞父进程

3) 支持作业控制

有关wait与waitpid的例子可以参考《深入理解计算机系统》P518页。

参考链接:https://blog.csdn.net/helloo_jerry/article/details/77336724

https://blog.csdn.net/yuanlairuci1992/article/details/52122381

什么是系统调用?

所谓系统调用,就是内核提供的、功能十分强大的一系列的函数。这些系统调用是在内核中实现的,再通过一定的方式把系统调用给用户,一般都通过门(gate)陷入(trap)实现。系统调用是用户程序和内核交互的接口。

重点可以参考:https://blog.csdn.net/justloveyou_/article/details/78304294

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值