《InterviewGuide》第六弹之操作系统提高篇

本文源自于个人github仓库:https://github.com/forthespada/InterviewGuide
github仓库内有PDF版本笔记下载方式,欢迎各位star、fork~
立志收录计算机校招、社招面试最全面试八股文,无内鬼来点八股文~

如果各位发现有些问题的图片上传失败,这是由于牛客不支持 github 图床自动上传,所以有时候图片上传成功、有时候上传失败...
这些问题的解答在github仓库上是好使的,不过还好有图片的八股文问题不多。对于这些图片上传失败的八股文问题,大家可以移步github看问题解答的答案。

22、操作系统经典问题之哲学家进餐问题

五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。

下面是一种错误的解法,如果所有哲学家同时拿起左手边的筷子,那么所有哲学家都在等待其它哲学家吃完并释放自己手中的筷子,导致死锁。

#define N 5

void philosopher(int i) {
    while(TRUE) {
        think();
        take(i);       // 拿起左边的筷子
        take((i+1)%N); // 拿起右边的筷子
        eat();
        put(i);
        put((i+1)%N);
    }
}

为了防止死锁的发生,可以设置两个条件:

  • 必须同时拿起左右两根筷子;
  • 只有在两个邻居都没有进餐的情况下才允许进餐。
#define N 5
#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N    // 右邻居
#define THINKING 0
#define HUNGRY   1
#define EATING   2
typedef int semaphore;
int state[N];                // 跟踪每个哲学家的状态
semaphore mutex = 1;         // 临界区的互斥,临界区是 state 数组,对其修改需要互斥
semaphore s[N];              // 每个哲学家一个信号量

void philosopher(int i) {
    while(TRUE) {
        think(i);
        take_two(i);
        eat(i);
        put_two(i);
    }
}

void take_two(int i) {
    down(&mutex);
    state[i] = HUNGRY;
    check(i);
    up(&mutex);
    down(&s[i]); // 只有收到通知之后才可以开始吃,否则会一直等下去
}

void put_two(i) {
    down(&mutex);
    state[i] = THINKING;
    check(LEFT); // 尝试通知左右邻居,自己吃完了,你们可以开始吃了
    check(RIGHT);
    up(&mutex);
}

void eat(int i) {
    down(&mutex);
    state[i] = EATING;
    up(&mutex);
}

// 检查两个邻居是否都没有用餐,如果是的话,就 up(&s[i]),使得 down(&s[i]) 能够得到通知并继续执行
void check(i) {         
    if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
        state[i] = EATING;
        up(&s[i]);
    }
}

23、操作系统经典问题之读者-写者问题

允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。

一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。

typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;

void reader() {
    while(TRUE) {
        down(&count_mutex);
        count++;
        if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
        up(&count_mutex);
        read();
        down(&count_mutex);
        count--;
        if(count == 0) up(&data_mutex);//最后一个读者要对数据进行解锁,防止写进程无法访问
        up(&count_mutex);
    }
}

void writer() {
    while(TRUE) {
        down(&data_mutex);
        write();
        up(&data_mutex);
    }
}

24、介绍一下几种典型的锁

读写锁

  • 多个读者可以同时进行读
  • 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
  • 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待

互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁

条件变量

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

自旋锁

如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

《互斥锁、读写锁、自旋锁、条件变量的特点总结》:https://blog.csdn.net/RUN32875094/article/details/80169978

  1. 线程(POSIX)锁有哪些?

    • 互斥锁(mutex)

      • 互斥锁属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程A和B,它们分别运行在core 0和core 1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞,此时会通过上下文切换将线程A置于等待队列中,此时core 0就可以运行其他的任务(如线程C)。
    • 条件变量(cond)

    • 自旋锁(spin)

      • 自旋锁属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,如果自旋锁已经被线程B所持有,那么线程A就会一直在core 0上进行忙等待并不停的进行锁请求,检查该自旋锁是否已经被线程B释放,直到得到这个锁为止。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。

      • 虽然它的效率比互斥锁高,但是它也有些不足之处:

        • 自旋锁一直占用CPU,在未获得锁的情况下,一直进行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,无疑会使CPU效率降低。

        • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。

      • 自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

25、逻辑地址VS物理地址

Eg:编译时只需确定变量x存放的相对地址是100 ( 也就是说相对于进程在内存中的起始地址而言的地址)。CPU想要找到x在内存中的实际存放位置,只需要用进程的起始地址+100即可。
相对地址又称逻辑地址,绝对地址又称物理地址。

26、怎么回收线程?有哪几种方法?

  • 等待线程结束:int pthread_join(pthread_t tid, void** retval);

    主线程调用,等待子线程退出并回收其资源,类似于进程中wait/waitpid回收僵尸进程,调用pthread_join的线程会被阻塞。

    • tid:创建线程时通过指针得到tid值。

    • retval:指向返回值的指针。

  • 结束线程:pthread_exit(void *retval);

    子线程执行,用来结束当前线程并通过retval传递返回值,该返回值可通过pthread_join获得。

    • retval:同上。
  • 分离线程:int pthread_detach(pthread_t tid);

    主线程、子线程均可调用。主线程中pthread_detach(tid),子线程中pthread_detach(pthread_sel

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

- 本专栏成功帮助阿秀拿到字节跳动SP的offer,脱胎于个人秋招时期的笔记总结。其中收纳C++(217道)、操作系统(62道)、计算机网络(100道)、数据结构与算法、数据库(MySQL、Redis)等高频问答知识点。 - 本专栏适合于校招、社招等找工作党,后来逐渐收录一些学弟学妹的上岸经验和方法,欢迎订阅,持续更新ing。

全部评论
感谢参与【创作者计划2期·技术干货场】!欢迎更多牛油来写干货,瓜分总计20000元奖励!!技术干货场活动链接:https://www.nowcoder.com/link/czz2jsghtlq(参与奖马克杯将于每周五结算,敬请期待~)
点赞 回复 分享
发布于 2021-03-26 11:07
前排提醒一下,我也不敢保证自己的总结都是对的,如果你们发现其中有错误,欢迎在下方留言,我会定期收集意见进行改正,感谢大家的监督!由于平台较多,无法一一修正因此只能以git仓库为标准答案。 仓库地址:https://github.com/forthespada/InterviewGuide github仓库内有PDF版本笔记下载方式,欢迎各位star、fork~       该系列目前已更新部分: 第一弹: 《InterviewGuide》第一弹之C++篇49问49答 https://www.nowcoder.com/discuss/597948?source_id=profile_create_nctrack&channel=-1 第二弹: 《InterviewGuide》第二弹C++进阶59问59答
点赞 回复 分享
发布于 2021-03-25 14:27
前排提醒,我也不敢担保自己总结的问题都是正确的,如果您发现我总结的知识点有错误的话,可以留言,我会定期修正!😂 别TM上来就喷人😪
点赞 回复 分享
发布于 2021-03-08 23:03
😉顶顶
点赞 回复 分享
发布于 2021-03-08 19:12

相关推荐

评论
6
37
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务