操作系统,揭开钢琴的盖子-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++**已经入门的学生或人士,有一定的编程基础。
本教程适合于互联网、嵌入式软件求职的学生或人士。
故事背景
**蒋 豆 芽:**小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。
**隔壁老李:**大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。
**导 师:**蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。
故事引入
导 师:豆芽,我一个眼神你明白了吧?
蒋 豆 芽:老师,没问题!论文刚把跌!(冲!)
豆芽面试字节躺平。。。。。。
面 试 官:哟,豆芽,又是你啊!
蒋 豆 芽:(嘿嘿)是啊。(我又杀回来了)
面 试 官:行,我们按照流程,你自我介绍下吧。
蒋 豆 芽:阿巴阿巴阿巴。。。。。。
面 试 官:首先第一个问题,你知道进程和线程的区别吗?
蒋 豆 芽:(心虚)进程我知道,进程是程序运行的封装,线程属于子进程。。。。。。
面 试 官:(会心一笑)豆芽基础还是不错的。我再问你,进程里的线程,哪些资源是共有的,哪些资源是独有的?
蒋 豆 芽:(心虚)堆栈是独有的吧?
面 试 官:(不置可否)我们看第二个问题,你知道什么是线程池吗?
蒋 豆 芽:(激动)我知道,就是把线程放进一个水池里。
面 试 官:(黑线)那我再问问你,你知道Linux虚拟内存机制吗?
蒋 豆 芽:不知道诶。
面 试 官:我们面试就到这里吧,后面有消息会联系你的。
经过惨烈吊打,豆芽当然不会收到任何消息。。。。。。
蒋 豆 芽:生活不易,豆芽叹气。老李,你快继续给我讲讲进程和线程吧,我面试又被吊打了。
1.16 线程
隔壁老李:哈哈,行吧,豆芽,别急,你听我慢慢讲。进程在早期的多任务操作系统中是基本的执行单元。一个进程就包含了程序指令和相关的资源集合,所有进程一起参与调度,竞争CPU、内存等系统资源。每次进程切换,都要先保存进程资源然后再恢复,这称为上下文切换。
比如我们有一个GUI程序,一般一个任务支持界面交互,另一个任务支持后台运算,那么分别用一个进程来处理。这样两个进程就将频繁切换引起额外开销,从而严重影响系统的性能。即使用进程的通信也很繁琐。
所以人们就想,能不能把这两个任务放到一个进程中,每个任务用一个更小粒度的执行单元来实现并发执行,这样就可以减少进程切换的开销了。线程的概念就很自然的产生了。
蒋 豆 芽:原来是这么回事啊。我明白了为什么要有线程了。
隔壁老李:一个进程里面可以包含多个线程,每个线程都可以执行一个任务。
所有线程都可以共享进程的资源,如代码段、数据段。但是每个线程都有自己的局部变量,需要栈存储,所有每个线程也有自己独立的资源,如栈、寄存器。这样,线程之间的切换只需要切换独立的资源,是不是开销自然就比进程低了呀!
蒋 豆 芽:(恍然大悟)没错!
隔壁老李:讲到这里,就要引出一个常见的面试题了:线程与进程的区别。
进程:进程是对程序运行时的封装,是系统资源调度的最小单位,对于单核CPU来说,实现了操作系统的并发;多核CPU可实现并行。
线程:线程是进程的子任务,是CPU调度的最小单位,实现了进程内部的并发。
区别:
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2)一个线程挂掉,对应的进程可能挂掉;一个进程挂掉,不会影响其他进程。
(3)进程是系统资源调度的最小单位;线程CPU调度的最小单位。
(4)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(5)进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
(6)进程切换时需要刷新TLB(Translation Lookaside Buffer,转译后备缓冲区,虚拟地址的缓存,用于改进虚拟地址到物理地址的转译速度)并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。
(7)通信方式不一样。
(8)进程适应于多核、多机分布;线程适用于多核
这里我们补充一下并行和并发的概念。同一时刻单个CPU只能处理一个任务,但不停地在多个任务来回快速切换,这就是并发。一个多核CPU可以同时处理多个任务,这就是并行。
1.17 线程的创建
隔壁老李:好了,我们接下来就要介绍用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
蒋 豆 芽:原来是这样!懂了!
隔壁老李:接下来我们就讲讲线程的通信方式。
一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。线程之间通过读取内存内容进行数据交换,试想如果不控制同步,一个线程在写,而另一个线程去读,就可能出现错误。同步的意思就是控制线程按次序执行的操作。
线程通信方式其实就是解决线程同步的问题。
有四种方式:
- 互斥锁。
- 信号量。
- 条件变量。
- 读写锁。
同步是指在一定的时间内只允许某一个线程访问某个资源。
蒋 豆 芽:(强行打断)老李,我想起来了,我们之前讲volatile关键字的时候就演示过,多个线程如果不进行同步控制,全局变量的读写将出现问题。
1.18 线程通信——互斥锁
隔壁老李:(会心一笑)不错啊,豆芽,看样子这些知识你都没有忘记。不错,所以我们要学会控制线程同步的方法。
(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>