\(OS\)知识点小记

写牛客题目时错了的都记一下
1.响应比高者优先作业调度算法是通过计算时间和周转时间来实现的。

调度算法:

感谢巨巨的博客

一.先来先服务(FCFS)

思想:
从“公平”的角度思考。
规则:
按照作业/进程到达的先后顺序
用于作业/进程调度:
作业调度:考虑那个作业先到达
进程调度:考虑那个进程先到达
抢占:
非抢占式的算法
优缺点:
优点:公平
缺点:对长作业有利,短作业不利

二.短作业优先(SJF)

思想:
追求最少的时间
规则:
按照作业/进程服务时间的大小
用于作业/进程调度:
一样,用于进程是被称之为(SPF)
抢占:
非抢占式的算法
存在抢占式的版本:最短剩余时间优先算法(SRTN)
优缺点:
优点:时间短
缺点:1.对短作业有利,长作业不利。2运行时间不一定准确。3可能会"饥饿":有源源不断地短作业进来。

3.高响应比优先(HRRNt)

思想:
综合考虑作业/进程的等待时间和要求服务的时间
规则:
在每次调度时计算各个作业的响应比

响应比 = (等待时间+要求服务时间)/(要求服务时间)
用于作业/进程调度:
一样
抢占:
非抢占式的算法
优缺点:
综合考虑了等待时间是和运行时间,也不会“饥饿"

2.通常说的“存储保护”的基本含义是防止程序间相互越界访问

存储保护

指给外置的存储设备价格保护程序,写不去数据,也删不掉数据。当多个用户共享主存的时候,为使系统能正常工作,应防止由于一个用户程序出错而破坏其他用户的程序和系统软件,还要防止一个用户程序不合法的访问不是分给他的主存区域。为此,系统提供存储保护。

通常采用的方法是:存储区域保护访问方式保护

3.关于多线程和多线程编程,以下哪些说法正确的()

A.多进程之间的数据共享比多线程编程复杂
B.多线程的创建,切换,销毁速度快于多进程
C.对于大量的计算优先使用多进程
D.多线程没有内存隔离,单个线程崩溃会导致整个应用程序的退出

解析:
A:不同进程有不同的页表,对应不同的物理地址空间,因此进程见需要使用IPC或者socket。而线程可以共享进程的地址空间。
B:进程创建需要OS给他分配内存,线程不用。
C:大量计算的时候需要频繁切换进程/线程,所以选线程
D:同意进程中的不同线程共享地址空间,所以一个线程挂掉会导致整个进程挂掉。

4.关于死锁的说法正确的有?

A.竞争可剥夺资源会产生死锁
B.竞争临时资源会产生死锁
C.在发生死锁时,必然存在一个进程—资源的环形链
D.如果进程在一次性申请其所需的全部资源成功后才运行,就不会发生死锁。

死锁的必要条件

  1. 互斥条件:指进程对所分配的资源进行排它性使用,也就是说在某一段时间内,某资源只被一个进程占用,如果还有其它进程请求该资源,则请求的进程只能等待,直到该资源被释放。
    方法: 如果允许系统资源都能共享使用,则系统不会进入死锁状态。
  2. 请求和保持条件:指一个进程已经拥有了某些资源,但是它还需要其它资源,于是又提出新的申请,而该资源又偏偏被其它进程占用,此时该申请资源的进程产生了阻塞, 它又不释放自己已占有的资源。
    方法:釆用预先静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不把它投入运行。一旦投入运行后,这些资源就一直归它所有,也不再提出其他资源请求,这样就可以保证系统不会发生死锁。
  3. 不剥夺条件:指进程已获得的资源不能被剥夺,只有自己使用完之后才释放.
    方法: 当一个已保持了某些不可剥夺资源的进程,请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着,一个进程已占有的资源会被暂时释放,或者说是被剥夺了,或从而破坏了不可剥夺条件。
  4. 环路等待条件:指发生死锁的时候,一定存在一个环路:进程1-资源2(被进程2占用)-资源3
    方法:为了破坏循环等待条件,可釆用顺序资源分配法。首先给系统中的资源编号,规定每个进程,必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源Ri,则该进程在以后的资源申请中,只能申请编号大于Ri的资源。
5.一个虚拟存储器系统中,设主存的容量为16MB,辅存的容量为1GB,而地址寄存器的位数32 位,

在这样的系统中,虚存的实际最大容量是\(1GB+16MB\)

虚拟存储区的最大容量 = \(min\)(内存+外存,\(2^n\))

虚拟存储器

感谢大佬 提出一个问题:有些程序需要很大的内存,但是计算机本身的物理内存不大。而且在多用户多任务系统中,多用户或多个任务共享全部主存,要求同时执行多道程序。这些同时运行的程序到底占用实际内存中的哪一部分?这我们是难以确定的,需要动态分配

solve:
程序运行时,分配给程序一定的运行空间,由地址转换不见将编程时的地址转换为实际内存的物理地址。如果内存不够,之调入正在运行的或将要运行的程序块,其余的放在辅存中。
一个大作业执行时,一部分地址空间在主存,一部分在辅存。当访问的信息不在主存,OS把信息从辅存调入主存。

这样我们仿佛有了一个巨大的存储器,但实际上这不是任何实际存在的物理存储器。它反复使用辅助存储器,来实现巨大的存储,这样的存储器就是——虚拟存储器

6.对记录式文件,操作系统为用户存取文件信息的最小单位是( )

A.字符
B.数据项
C.记录
D.文件

逻辑文件

文件的逻辑结构是用户可见结构。逻辑文件从结构上分成两种形式:
一种是无结构的流式文件,信息没有单位,他是依靠数据流构成的文件。
一种是有结构的记录式文件,每一个单位称之为逻辑记录。记录通常描述一个实体集,有着不同或相同的数据项,记录的长度可分为定长和不定长两种

7.操作系统采用缓冲技术主要是通过硬件来实现的。(错)

缓冲区是一个存储区域,他可以由硬件实现,但是成本较高,一般用于速度要求比较高的地方。
一般来说,利用内存作为缓冲区,如单缓冲区,双缓冲区、环形缓冲区和缓冲池。

软链接和硬链接

什么是链接?

链接简单说实际上是一种文件共享的方式。

软链接又叫符号链接,文件包含了另一个文件的路径名。可以是任意文件或目录,可以连接不同文件系统的文件。

硬链接就是一个文件的一个或多个文件名。

限制:
硬链接只能对已存在的文件创建,
软链接可以对没存在的创建

创建方式:
因链接不能对目录创建,只可对文件创建
软链接可对文件或目录创建

影响:
删除一个硬链接文件不影响其他相同的inode的文件。
删除软连接不影响被指向的文件。

大端小端

小端模式:低的有效字节存储在低的存储器地址。小端一般是主机字节序。
大端模式:高的有效字节存储在低的存储器地址。大端为网络字节序。可用于网络传输,可以先判断报文信息。
TCP/IP协议规定第一个字节是高字节。

void check(){
    union test{
        char c;
        int i;
    }
    test t;t.i = 1
    puts(t.c == 1?"BIG":"SMALL");
}

进程调度算法

除了之前的还有:
4.时间片轮转算法:
每次调度八CPU分配给队首进程,并令其执行一个时间片。时间片的大小是从几ms到几百ms。当执行
5.多级反馈队列调度算法:综合前面多种调度算法

操作系统如何申请以及管理内存的?

1.物理内存:寄存器、高速缓存、主存、磁盘
操作系统的内存管理器,它的主要工作是有效的管理内存。
2.虚拟内存:操作系统为每一个进程分配一个独立的地址空间。虚拟内存和物理内存存在映射关系。

局部性原理

时间:之前被访问的数据很有可能再次被访问
空间:数据和程序都有聚集成群的倾向

磁盘预读

内存和磁盘在进行交互时有一个最小的逻辑单位——页,4k或者8k,innoDB读取的时16kb的数据

简述Linux系统态与用户态,什么时候会进入系统态?

内核态与用户态:内核态(系统态)与用户态是操作系统的两种运行级别。内核态拥有最高权限,可以访问所有系统指令;用户态则只能访问一部分指令。

什么时候进入内核态:共有三种方式:a、系统调用。b、异常。c、设备中断。其中,系统调用是主动的,另外两种是被动的。

为什么区分内核态与用户态:在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。所以区分内核态与用户态主要是出于安全的考虑。

多线程循环打印ABC

#include<bits/stdc++.h>
using namespace std;

mutex mtx;
condition_variable cvar;

char ch = 'A',arr[] = {'A','B','C'};

void print(int i){
    for(int j = 0;j < 10;j++){
        unique_lock<mutex> lk(mtx);
        cvar.wait(lk,[=] {
            return ch == arr[i];
        });
        cout<<arr[i];
        ch = arr[(i+1)%3];
        lk.unlock();
        cvar.notify_all();
    }
}

int main(){
    vector<thread> threads;
    threads.push_back(thread(print,'A'));
    threads.push_back(thread(print,'B'));
    threads.push_back(thread(print,'C'));

    threads[0].join();
    threads[1].join();
    threads[2].join();
    cout<<endl;
    return  0;
}

锁的类型

互斥锁( mutexlock ):

最常使用于线程同步的锁;标记用来保证在任一时刻,只能有一个线程访问该对象,同一线程多次加锁操作会造成死锁;临界区和互斥量都可用来实现此锁,通常情况下锁操作失败会将该线程睡眠等待锁释放时被唤醒

自旋锁(spinlock):

同样用来标记只能有一个线程访问该对象,在同一线程多次加锁操作会造成死锁;使用硬件提供的swap指令或test_and_set指令实现;同互斥锁不同的是在锁操作需要等待的时候并不是睡眠等待唤醒,而是循环检测保持者已经释放了锁,这样做的好处是节省了线程从睡眠状态到唤醒之间内核会产生的消耗,在加锁时间短暂的环境下这点会提高很大效率

读写锁(rwlock):

高级别锁,区分读和写,符合条件时允许多个线程访问对象。处于读锁操作时可以允许其他线程和本线程的读锁, 但不允许写锁, 处于写锁时则任何锁操作都会睡眠等待;常见的操作系统会在写锁等待时屏蔽后续的读锁操作以防写锁被无限孤立而等待,在操作系统不支持情况下可以用引用计数加写优先等待来用互斥锁实现。 读写锁适用于大量读少量写的环境,但由于其特殊的逻辑使得其效率相对普通的互斥锁和自旋锁要慢一个数量级;值得注意的一点是按POSIX标准 在线程申请读锁并未释放前本线程申请写锁是成功的,但运行后的逻辑结果是无法预测

递归锁(recursivelock):

严格上讲递归锁只是互斥锁的一个特例,同样只能有一个线程访问该对象,但允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作; windows下的临界区默认是支持递归锁的,而linux下的互斥量则需要设置参数PTHREAD_MUTEX_RECURSIVE_NP,默认则是不支持

进程通信方式

管道:

无名管道:数据单向流动,只能在有亲缘关系的进程间使用。
有名管道:允许在没有亲缘关系的进程之间使用。

共享内存:

一个进程创建,多个进程可以访问。共享内存是最快的IPC方式,往往和信号量配合使用。

消息队列:

有消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服信号传递少、管道只能承受五个师字节流以及缓冲区大小受限。

socket

适用与不同机器间进程通信,在本地也可作为两个进程通信的方式。

信号:

用于通知接收进程某个事件已经发生。

信号量

一个计数器,控制多个进程对共享资源的访问。它常作为一种锁机制,实现进程、线程对临界区的同步和互斥。

生产者消费者模型

使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。

const int N = 150;
int mutex = 1;
int empty = N;
int full = 0;

void producer(){
    while(true){
        int item = produce_item();
        empty--;
        mutex--;

        insert(item);
        
        mutex++;
        full++;
    }
}

void consumer(){
    while(true){
        full--;
        mutex--;
      
        int item = remove_item();

        consume(item);
      
        mutex++;
        empty++;
    }
}

缺页中断

  1. 缺页异常:malloc和mmap函数再分配内存时只是建立了进程虚拟地址空间,没有分配虚拟内存对应的物理地址。进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发缺页异常,引发缺页中断
  2. 缺页中断:缺页异常后将产生一个缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存

页面置换算法

//今天面字节问了这个,不自信没说出答案(好吧其实是自己垃圾)

当产生缺页中断时,需要选择一个页面写入。如果要换出的页面在内存中被修改过,变成了“脏”页面,那就需要先写会到磁盘。页面置换算法,就是要选出最合适的一个页面,使得置换的效率最高。

1.最佳置换算法(OPT)

每次选择淘汰的页面时以后很长时间不使用的。

OPT是一种理想化的算法,实际上我们没有办法知道哪一个页面会被访问,所以OPT更多的时候是作为一把尺子来丈量其他页面置换算法的效率。

2.先进先出置换算法(FIFO)

每次选择淘汰的页面最早进入内存的页面。

缺点:
会有Belady异常:当为进程分配的物理块数增大时,缺页次数不减反增的异常现象。
只有FIFO算法会产生Belady异常,且算法性能差。

3.最久未使用算法(LRU)

每次淘汰的页面是最久未使用的页面。

在手动做题时,若需要淘汰页面,可以逆向检查此时在内存中的几个页面号。在逆向扫描过程中最后一个出现的页号就是要淘汰的页面。

class LRUCache {
    list<pair<int, int>> cache;//创建双向链表
    unordered_map<int, list<pair<int, int>>::iterator> map;//创建哈希表
    int cap;
public:
    LRUCache(int capacity) {
        cap = capacity;
    }
    
    int get(int key) {
        if (map.count(key) > 0){
            auto temp = *map[key];
            cache.erase(map[key]);
            map.erase(key);
            cache.push_front(temp);
            map[key] = cache.begin();//映射头部
            return temp.second;
        }
        return -1;
    }
    
    void put(int key, int value) {
        if (map.count(key) > 0){
            cache.erase(map[key]);
            map.erase(key);
        }
        else if (cap == cache.size()){
            auto temp = cache.back();
            map.erase(temp.first);
            cache.pop_back();
        }
        cache.push_front(pair<int, int>(key, value));
        map[key] = cache.begin();//映射头部
    }
};

4.时钟置换算法

时钟置换算法是一种性能和开销较均衡的算法,又称CLOCK算法,或最近未用算法。

(还没明白)

多线程访问共享资源的时候,避免不了资源竞争而导致数据错乱的问题,所以我们会选择加锁。

互斥锁和自旋锁:

当已经有一个线程加锁后,其他线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的:

  • 互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
  • 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;

互斥锁是一种独占锁:A加锁成功后,B就加锁失败,线程B释放了CPU,加锁的代码进入阻塞区。

加锁失败时会切换线程,也就是说会有两次线程上下文切换的成本。

假如B进程的代码执行时间很快,上下文切换的时间的比代码执行的时间要快,那么我们就不该使用互斥锁,应该使用自旋锁。

自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。
自旋锁加锁:
一:查看锁的状态,如锁是空闲的,则执行二
二:将锁设置为当前线程持有

多线程竞争的时候,加锁失败的线程会忙等待,直到拿到锁。

处于忙等待的线程可以看作自旋,他们会一直自旋直到锁可以用。单核CPU上,需要抢占式的调度器,否则单CPU上无法使用,因为自旋的线程不会放弃CPU。

自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。

读写锁

由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。

读写锁的工作原理是:
当写锁没有被线程持有时,多个线程可以并发的持有锁,大大提高了共享资源的访问效率,因为读锁用于读取共享资源,所以多个线程同时持有读锁也不会破坏共享资源的数据。

写锁一但被线程持有,读线程的获取读锁的操作会被阻塞,其他写线程也会阻塞。

所以说,写锁是独占锁,任何一个时刻只能有一个线程持有写锁,读锁是共享锁,读锁可以被多线程持有。

读优先锁:
读线程A持有读锁,写线程B会阻塞,而剩下的读线程C可以继续读锁,直到A、C全部释放读锁,B线程才可以写锁。
简单来说,必须把所有的读线程结束才可以进行写线程,但如果一直有读线程,那么先到的写线程就有可能会产生饥饿现象。

写优先锁:
读线程A持有读锁,写线程B会阻塞,而剩下的读线程C可不可以继续读锁,只要A线程释放读锁,B线程就可以写锁。
同样的,写优先锁也有可能会使读锁线程饥饿。

公平读写锁:
用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁,这样读线程依然可以并发。

乐观锁和悲观锁

乐观锁和悲观锁的分类方式是根据主观态度来区分的。
悲观锁:
他认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。

乐观锁:
先修改共享资源,再验证这段时间有没有冲突,如果没有其他线程修改资源,操作Ok,如果发现有其他线程已经修改了这个资源,放弃操作。

多路复用