操作系统,揭开钢琴的盖子-5

C++软件与嵌入式软件面经解析大全(蒋豆芽的秋招打怪之旅)


本章讲解点

  • 1.1 操作系统的来历
  • 1.2 操作系统的功能
  • 1.3 操作系统的硬件知识
  • 1.4 linux下编译程序
  • 1.5 Makefile
  • 1.6 linux的常用指令
  • 1.7 进程的概念
  • 1.8 进程的状态转换
  • 1.9 进程的创建
  • 1.10 守护进程
  • 1.11 僵尸进程与孤儿进程
  • 1.12 wait()或waitpid()系统调用
  • 1.13 进程通信——管道
  • 1.14 进程通信——系统IPC
  • 1.15 进程通信——socket套接字
  • 1.16 线程
  • 1.17 线程的创建
  • 1.18 线程通信——互斥锁
  • 1.19 线程通信——信号量
  • 1.20 线程通信——条件变量
  • 1.21 线程通信——读写锁
  • 1.22 线程池
  • 1.23 协程
  • 1.24 虚拟内存
  • 1.25 页表
  • 1.26 缺页中断
  • 1.27 缺页置换算法
  • 1.28 锁
  • 1.29 操作系统资源调度方法
  • 1.30 IO模型类型

受众:本教程适合于**C/C++**已经入门的学生或人士,有一定的编程基础。

本教程适合于互联网嵌入式软件求职的学生或人士。

img

故事背景

img

**蒋 豆 芽:**小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。

**隔壁老李:**大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。

**导 师:**蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。

img

故事引入

img

导 师:豆芽,我一个眼神你明白了吧?

蒋 豆 芽:老师,没问题!论文刚把跌!(冲!)


豆芽面试字节躺平。。。。。。


面 试 官:哟,豆芽,又是你啊!

蒋 豆 芽:(嘿嘿)是啊。(我又杀回来了)

面 试 官:行,我们按照流程,你自我介绍下吧。

蒋 豆 芽:阿巴阿巴阿巴。。。。。。

面 试 官:首先第一个问题,你知道进程和线程的区别吗?

蒋 豆 芽:(心虚)进程我知道,进程是程序运行的封装,线程属于子进程。。。。。。

面 试 官:(会心一笑)豆芽基础还是不错的。我再问你,进程里的线程,哪些资源是共有的,哪些资源是独有的?

蒋 豆 芽:(心虚)堆栈是独有的吧?

面 试 官:(不置可否)我们看第二个问题,你知道什么是线程池吗?

蒋 豆 芽:(激动)我知道,就是把线程放进一个水池里。

面 试 官:(黑线)那我再问问你,你知道Linux虚拟内存机制吗?

蒋 豆 芽:不知道诶。

面 试 官:我们面试就到这里吧,后面有消息会联系你的。


经过惨烈吊打,豆芽当然不会收到任何消息。。。。。。


蒋 豆 芽:生活不易,豆芽叹气。老李,你快继续给我讲讲进程和线程吧,我面试又被吊打了。

img

1.16 线程

img

隔壁老李:哈哈,行吧,豆芽,别急,你听我慢慢讲。进程在早期的多任务操作系统中是基本的执行单元。一个进程就包含了程序指令和相关的资源集合,所有进程一起参与调度,竞争CPU、内存等系统资源。每次进程切换,都要先保存进程资源然后再恢复,这称为上下文切换

比如我们有一个GUI程序,一般一个任务支持界面交互,另一个任务支持后台运算,那么分别用一个进程来处理。这样两个进程就将频繁切换引起额外开销,从而严重影响系统的性能。即使用进程的通信也很繁琐。

所以人们就想,能不能把这两个任务放到一个进程中,每个任务用一个更小粒度的执行单元来实现并发执行,这样就可以减少进程切换的开销了。线程的概念就很自然的产生了。

蒋 豆 芽:原来是这么回事啊。我明白了为什么要有线程了。

隔壁老李:一个进程里面可以包含多个线程,每个线程都可以执行一个任务。

所有线程都可以共享进程的资源,如代码段、数据段。但是每个线程都有自己的局部变量,需要栈存储,所有每个线程也有自己独立的资源,如栈、寄存器。这样,线程之间的切换只需要切换独立的资源,是不是开销自然就比进程低了呀!

蒋 豆 芽:(恍然大悟)没错!

隔壁老李:讲到这里,就要引出一个常见的面试题了:线程与进程的区别

进程:进程是对程序运行时的封装,是系统资源调度的最小单位,对于单核CPU来说,实现了操作系统的并发;多核CPU可实现并行。

线程:线程是进程的子任务,是CPU调度的最小单位,实现了进程内部的并发。

区别:

(1)一个线程从属于一个进程;一个进程可以包含多个线程。

(2)一个线程挂掉,对应的进程可能挂掉;一个进程挂掉,不会影响其他进程。

(3)进程是系统资源调度的最小单位;线程CPU调度的最小单位。

(4)进程系统开销显著大于线程开销;线程需要的系统资源更少。

(5)进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。

(6)进程切换时需要刷新TLB(Translation Lookaside Buffer,转译后备缓冲区,虚拟地址的缓存,用于改进虚拟地址到物理地址的转译速度)并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。

(7)通信方式不一样。

(8)进程适应于多核、多机分布;线程适用于多核

这里我们补充一下并行和并发的概念。同一时刻单个CPU只能处理一个任务,但不停地在多个任务来回快速切换,这就是并发。一个多核CPU可以同时处理多个任务,这就是并行。

img

1.17 线程的创建

img

隔壁老李:好了,我们接下来就要介绍用C语言如何创建线程。

创建多线程需要包括的头文件:

#include <pthread.h>
  • 创建线程,函数原型如下:

    typedef unsigned long int pthread_t  
    //函数原型:  
    int pthread_create(  
        pthread_t *thread,  
        const pthread_attr_t *attr,  
        void *(*start_routine)(void*),   
        void *arg   
    );  
    	  
    //参数;  
        //thread:指向线程标识符的指针  
        //attr:设置线程属性  
        //start_routine:线程回调函数的起始地址  
        //arg:回调函数的参数  
      
    //返回值  
        //成功返回0,thread 指向的内存单元将被设置为新创建线程的线程ID号  
        //失败返回错误号,并且*thread 中的内容是未定义的  
    
  • 等待一个线程结束,函数原型如下:

    typedef unsigned long int pthread_t  
    //函数原型:  
    int pthread_join(  
        pthread_t thread,  
        void ** retval  
    );  
      
    //参数;  
        //thread:被等待的线程标识符  
        //retval:一个用户定义的指针,它可以用来存储被等待线程的返回值  
      
    //返回值  
        //成功返回时,被等待的线程的资源被回收  
    
  • 一个线程的结束有两种途径:一种是任务完成后自动线程结束,另一种是通过函数pthread_exit来实现。函数原型如下:

    void pthread_exit (void *retval);  
    

    唯一的参数是函数的返回代码。

    pthread_join和pthread_exit的区别如下所述。

    (1)pthread_join 一般是主线程来调用,用来等待子线程退出,因为是等待,所以主线程将被阻塞。

    (2)ptbread_exit一般是子线程调用,用来结束当前线程。

    (3)子线程可以通过pthread_exit传递一个返回值,而主线程通过pthread_join获得该返回值,从而判断该子线程的退出是正常还是异常。

  • 捕获线程id,函数原型如下:

    pthread_t pthread_self(void);  
    
  • 使得主线程与子线程分离(主线程不阻塞,继续运行),子线程结束后,资源由init进程自动回收,函数原型如下:

    int pthread_detach(pthread_t tid);
    //参数;  
        //tid:线程标识符  
    	  
    //返回值  
        //pthread_detach() 在调用成功完成之后返回零。其他任何返回值都表示出现了错误。如果检测到以下任一情况,pthread_detach()将失败并返回相应的值。  
    

隔壁老李:好了我们给出一个实例。

蒋 豆 芽:来了来了,又来了,头皮发麻,手指发抖,小笔记又快写不过来了,老李,你慢点。

隔壁老李:(笑容邪魅,语速加快)这个实例创建了一个线程,调用回调函数打印“hello douya!”和传入的参数,同时捕获线程id。pthread.c实例如下:

#include <stdio.h>  
#include <pthread.h>  
void *func(void *args){  
    int i = *(int *)args;  
    printf("hello douya, i = %d, tid = %lu\n", i,pthread_self());  
    pthread_exit((void *)1);  //这里子线程自己退出,返回一个值来标识是否正常退出
}  
int main(){  
    pthread_t tid;  
    int param = 1;  // 传入参数1
    int result = pthread_create(&tid, NULL, func, (void *)param );  // 传入了一个指针函数func
    if (result != 0){  
        printf("pthread creates the error! return value = %d\n",result);  
        return result;  
    }  
    printf("pthread id = %lu\n",tid);  
    void *retval;  
    result = pthread_join(tid, &retval);  //这里需要等待我们的子线程结束。我们的main函数也是一个线程,这里属于主线程,那么main函数就将阻塞,等待子线程结束后,main函数才能继续运行后面的语句。
    if (result != 0){  
        printf("pthread joins error! return value = %d\n",result);  
        return result;  
    }  
    printf("retval = %ld\n",(long)retval);  
    return 0;  
}  

我们输入gcc -Wall pthread.c -o pthread -lpthread进行编译,在gcc编译的时候,附加要加**-lpthread参数**,因为pthread不是linux下的默认的库,所以我们需要链接此库。

运行结果如下:

$ ./pthread
pthread id = 140669279119104
hello douya, i = 1, tid = 140669279119104
retval = 1

蒋 豆 芽:原来是这样!懂了!

隔壁老李:接下来我们就讲讲线程的通信方式。

一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。线程之间通过读取内存内容进行数据交换,试想如果不控制同步,一个线程在写,而另一个线程去读,就可能出现错误。同步的意思就是控制线程按次序执行的操作。

线程通信方式其实就是解决线程同步的问题

有四种方式:

  1. 互斥锁
  2. 信号量
  3. 条件变量
  4. 读写锁

同步是指在一定的时间内只允许某一个线程访问某个资源。

蒋 豆 芽:(强行打断)老李,我想起来了,我们之前讲volatile关键字的时候就演示过,多个线程如果不进行同步控制,全局变量的读写将出现问题。

img

1.18 线程通信——互斥锁

img

隔壁老李:(会心一笑)不错啊,豆芽,看样子这些知识你都没有忘记。不错,所以我们要学会控制线程同步的方法。

(1)互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁(lock)和解锁(unlock)。

互斥锁一般被设置成全局变量。打开的互斥锁可以由某个线程获得。一旦获得,这个互斥锁会锁上,此后只有该线程有权打开,其他想要获得互斥锁的线程,会等待直到互斥锁再次打开的时候。

我们可以将互斥锁想象成一个只能容纳一个人的洗手间,当某个人进入洗手间的时候,可以从里面将洗手间锁上,其他人只能在互斥锁外面等待那个人出来,才能进去。但在外面等候的人并没有排队,谁先看到洗手间空了,就可以首先冲进去。

蒋 豆 芽:还真是很形象了啊,笑死我,哈哈。

隔壁老李:我们先看看互斥锁函数原型:

//头文件  
#include <pthread.h>  
#include <time.h>  
//初始化一个互斥锁  
int pthread_mutex_init(pthread_mutex_t *mutex,//互斥锁标识  
                 const pthread_mutexattr_t *attr//互斥锁属性  
                 );  
  
// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,  
// 直到互斥锁解锁后再上锁。  
int pthread_mutex_lock(pthread_mutex_t *mutex);  
	  
// 调用该函数时,若互斥锁未加锁,则上锁,返回 0;  
// 若互斥锁已加锁,则函数直接返回失败,即 BUSY  
int pthread_mutex_trylock(pthread_mutex_t *mutex);  
  
// 对指定的互斥锁解锁。  
int pthread_mutex_unlock(pthread_mutex_t *mutex);  
  
// 销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。  
int pthread_mutex_destroy(pthread_mutex_t *mutex);  

我们给出实例:

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <pthread.h>  
#include <string.h>  
	   
char *buf[128]; //字符指针数组,全局变量  
int pos = 0; //用于指定上面数组的下标  
   
//第一步:定义互斥量  
pthread_mutex_t mutex;  
  
void *task(void *p);  
  
int main(void){  
    //第二步:初始化互斥量, 默认属性  
    pthread_mutex_init(&mutex, NULL);  
   
    //1.创建线程 向数组中存储内容  
    pthread_t tid1, tid2, tid3;  
    pthread_create(&tid1, NULL, task, (void *)"zhangfei");  
    pthread_create(&tid2, NULL, task, (void *)"guanyu");  
    pthread_create(&tid3, NULL, task, (void *)"liubei");  
    //2.主线程等待,并且打印最终的结果  
    pthread_join(tid1, NULL);  
    pthread_join(tid2, NULL);  
    pthread_join(tid3, NULL);  
    
    printf("字符指针数组中的内容是:\n");  
    for(int j = 0; j < pos; ++j){  
        printf("%s \n", buf[j]);  
    }  
    //第五步:销毁互斥量  
    pthread_mutex_destroy(&mutex);  
    return 0;  
}  
  
void *task(void *p){  
    //第三步:使用互斥量进行加锁  
    pthread_mutex_lock(&mutex);  
    buf[pos] = (char *)p;  
    sleep(1);  //这里线程睡眠1s,在这个空闲里,其他线程将继续执行
    pos++;  
    //第四步:使用互斥量进行解锁  
    pthread_mutex_unlock(&mutex);  
}  

程序运行的结果就是三个字符串按顺序打印,实现了同步。

$ ./lock
字符指针数组中的内容是:
zhangfei
guanyu
liubei
    
$ ./lock
字符指针数组中的内容是:
zhangfei
guanyu
liubei
    
$ ./lock
字符指针数组中的内容是:
zhangfei
guanyu
liubei

但是如果我们去掉互斥锁,就会导致多线程激烈竞争,读写错误。

$ ./non-lock
字符指针数组中的内容是:
liubei
(null)
(null)
    
$ ./non-lock
字符指针数组中的内容是:
guanyu
(null)
(null)
    
$ ./non-lock
字符指针数组中的内容是:
liubei
(null)
(null)

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

<p> - 本专刊适合于C/C++已经入门的学生或人士,有一定的编程基础。 - 本专刊适合于互联网C++软件开发、嵌入式软件求职的学生或人士。 - 本专刊囊括了C语言、C++、操作系统、计算机网络、嵌入式、算法与数据结构等一系列知识点的讲解,并且最后总结出了高频面试考点(附有答案)共近400道,知识点讲解全面。不仅如此,教程还讲解了简历制作、笔试面试准备、面试技巧等内容。 </p> <p> <br /> </p>

全部评论
请问豆芽哥,现在这些内容还在更新吗
点赞 回复 分享
发布于 2023-05-13 16:05 韩国
C++有协程了,豆芽考虑补一下嘛😁
点赞 回复 分享
发布于 2022-08-13 10:08
线程通信方式的引言部分:“线程通信方式其实就是解决线程同步的问题”,这句话感觉可以稍微扩展一下,会使小白记忆更深刻。 父子进程除代码区共享外,其余数据段和堆栈段是各自独立的,因此进程间的通信主要用来进行数据交换。线程共享了主程序的绝大部分空间,线程要写这些共享数据时反而要加以保护,所以必须要用各种锁来同步线程。因此,线程通信方式其实就是解决线程同步的问题。
点赞 回复 分享
发布于 2021-08-16 20:27
1.21 读写锁,前面说 “读写锁一次只允许一个线程写,但允许一次多个线程读”,后面又出现“我们上面说了一句话豆芽你要注意:一次只有一个线程可以对其加锁,不论是加读锁还是加写锁。” 请问豆芽,这两句话是否矛盾呢,只允许一个线程加锁,怎么实现多个线程读呢?
点赞 回复 分享
发布于 2021-07-29 15:12
1.17的实例中 pthread_create(&tid, NULL, func, param ); 最后一个param是不是要改成&param呀?
点赞 回复 分享
发布于 2021-06-09 19:41
1.18实例中的代码,我有一点不理解,join在线程还没有结束的时候会阻塞,为什么在阻塞后线程都结束了,还会在主线程后面打印原来传入线程中的字符指针数组的内容。
点赞 回复 分享
发布于 2021-05-26 10:13

相关推荐

08-19 19:57
石河子大学 C++
企鹅百度字节的孝子:为啥本科只有两年啊
校招求职吐槽
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

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