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

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

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

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


豆芽继续埋头干面经。。。。。。


蒋 豆 芽:(笑容邪魅)老李,你上次讲的很好啊!

隔壁老李:嗯哼,谢谢夸奖哦!

蒋 豆 芽:(暗示)我说你讲的很好啊!

隔壁老李:(得意)我知道啊,我确实讲得好,这一点我很清楚,别人都说我有当老师的气质呢!

蒋 豆 芽:不是,不是这个意思。

隔壁老李:(困惑)那你什么意思?

蒋 豆 芽:我是说,你不打算再继续讲讲?

隔壁老李:(笑容邪魅)我就知道你想说啥,哈哈!

蒋 豆 芽:(不好意思)嘿嘿,大佬,再讲讲吧,操作系统的知识好多都不懂啊,救救豆芽吧!

隔壁老李:哈哈,好吧。上次我们讲了一些预先知识,为后面做铺垫,今天就是要讲我们的重点知识了!

蒋 豆 芽:(激动)好的,老李,来,赶紧上车!

img

1.7 进程概念

img

隔壁老李:我们先来看看进程的概念。程序是指令、数据及其组织形式的描述,而进程则是程序的运行实例,包括程序计数器、寄存器和变量的当前值。

我们上一节讲了并发与并行,对于单个CPU而言,一个时刻只能运行一个进程,这自然是个并发的过程。如图:一个时刻内只有一个进程在运行。

img

隔壁老李:然后我们来看一下Linux的进程结构,一般分为三部分代码段、数据段(.data与.bss)和堆栈段

代码段用于存放程序代码,如果有多个进程运行相同的一个程序,那么它们可以使用同一个代码段。代码段还会存储一部分常量,如字符串常量字面值。

数据段则存放程序的全局变量和静态变量。

堆栈段中的栈用于函数调用,存放着函数的参数、局部变量。

蒋 豆 芽:(疑惑)那么程序是怎么转换为进程的呢?

隔壁老李:这个问题问得很好。听我慢慢讲。我们之前已经讲过,一个程序的生成分为四个阶段预编译、编译、汇编和链接,最后生成可执行文件。当程序执行时,操作系统将可执行文件复制到内存中,然后经过以下几个步骤转换为进程

  1. 内核将程序读入内存,为程序分配内存空间。
  2. 内核为该进程分配进程标识符PID,记住这个名称)和其他所需资源
  3. 内核为进程保存PID及相应的状态信息,并且将进程放入运行队列中等待执行。
  4. 由操作系统调度执行。

我们来举个例子,比如我们写一个C文件,里面包含了一个main函数,在操作系统里编译然后运行,经过上述一系列步骤,那么操作系统就将这个执行程序“包装”成一个进程,进程里面再创建一个线程来执行这个可执行文件。过程就是如此。

这里解释一下,为什么还要创建一个线程,因为我们现在的操作系统是多线程的体系,一个进程里可以包含多个线程,至少有一个线程。以前的操作系统是单道或多道系统,即进程直接执行程序,那么就跟线程没关系了。我们后面的讲解都是基于多线程体系来的。这一点豆芽你要注意。


隔壁老李:一个进程对应唯一标识符PID。标识符类型为pid_t,是一个无符号整型。同一个可执行程序可以被加载为多个不同的进程。因此进程与PID是一对一关系,而进程与程序文件之间是多对一关系。

蒋 豆 芽:(满头大汗)老李,你慢点,小笔记写不过来了。

img

1.8 进程五种状态

img

隔壁老李:(语速加快)进程有五种状态:创建、就绪、执行、阻塞、终止。一个进程创建后,被放入队列处于就绪状态,等待操作系统调度执行,执行过程中可能切换到阻塞状态(并发),任务完成后,进程销毁终止。如图:

img

创建状态
一个应用程序从系统上启动,首先就是进入创建状态,需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配。

就绪状态
创建状态完成之后,进程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。

运行状态
获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态

阻塞状态
运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态

终止状态
进程结束或者被系统终止,进入终止状态

蒋 豆 芽:(不行了,要打断老李)那老李,我们怎么创建、销毁一个进程呢?

img

1.9 进程创建方式

img

隔壁老李:既然我们要让一个程序(可执行文件)运行起来,那么自然就要创建进程了呀,进程的创建方式有两种:一种由操作系统创建一种由父进程创建

  1. 我们先讲由操作系统创建的进程。在系统启动时,操作系统会创建一些进程,它们承担着管理和分配资源的任务,这些进程维持这系统的稳定运行,被称为系统进程

  2. 另一种方式就是由父进程创建。系统允许一个进程创建新进程(即子进程),子进程又可以创建新的子进程,形成树结构。子进程创建成功后,子进程将存在于系统之中,并且独立于父进程。子进程可以接受系统调度,可以分配资源。那么创建一个子进程,常用fork()函数,其原型如下:

    #include <unistd.h>  
    pid_t fork(void);  
    
    /*
    fork()函数不需要参数,返回值是一个进程标识符PID。返回值有以下三种情况:
    (1)    对于父进程,fork()函数返回新创建的子进程的PID。
    (2)    对于子进程,fork()函数调用成功会返回0。
    (3)    如果创建出错,fork()函数返回-1。
    */

    fork()函数创建一个新进程后,会为这个新进程分配进程空间,将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段

    这时候,子进程和父进程一模一样,都接受系统的调度。因为两个进程都停留在fork()函数中,最后fork()函数会返回两次,一次在父进程中返回,一次在子进程中返回,两次返回的值不一样,如上面的三种情况。

蒋 豆 芽:具体怎么创建呢?(接下来老李要写代码了,我可以休息下了,吁。)

隔壁老李:(拿出写好的用例)接下来我们给出用例:这个例子让父子进程分别获取PID。豆芽,快点记。

蒋 豆 芽:(满头大汗)遵命。

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
int main(){  
    pid_t pid = fork();  
    if (pid < 0){  
        printf("fork error!");  
        exit(-1); //退出进程  
    }  
    else if (pid == 0)  
        //子进程  
        printf("Child--PID : %u, Parent--PID : %u\n",getpid(),getppid());  
    else{  
        //父进程  
        printf("Parent--PID : %u, Child--PID : %u\n",getpid(),pid);  
        sleep(2); //休眠2s,让子进程先运行  
    }  
    return 0;  
} 

运行结果如下:

Child--PID : 4, Parent--PID : 3
Parent--PID : 3, Child--PID : 4

getpid()用于获得当前进程的pid,而getppid()则是获取父进程的pid。

exit()函数可以退出进程,原型如下:

#include <stdlib.h>  
void exit(int status); 

exit()函数的参数表示进程的退出状态,0表示进程正常退出;其他的值表示进程异常退出。

这时我们就成功使用fork函数创建了子进程。

蒋 豆 芽:(赞同)嗯嗯,原来如此,看着很直观。

隔壁老李:再次强调,子进程完全复制了父进程的地址空间的内容,包括数据段和堆栈段的内容。但是子进程却和父进程共享代码段,这个好理解,因为代码段只读,不会被修改,那么为节约进程空间,共享就行了。

隔壁老李:而在Linux的后续发展中,当创建新进程时,连数据段和堆栈段都不再立马复制了,而是等到需要修改数据段或堆栈段的数据时再复制,这就是写时复制

这样更加节省了进程空间,效率更高。豆芽,你明白了吗?面试官经常问哦,要好好记住。

蒋 豆 芽:没问题!老李,你讲得很清楚啊,困扰我多年的进程我今天终于明白了。

img

1.10 守护进程

img

隔壁老李:???才开始学操作系统,就多年的困扰了?

蒋 豆 芽:(囧)人艰不拆嘛。

隔壁老李:(嘻嘻)前面我们提到了系统进程,其中有一种特殊的进程,那就是守护进程,面试也喜欢问,我们就来介绍一下。

守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端,处理一些系统级别任务。

创建过程如下:

  1. 创建子进程,终止父进程。方法是调用fork()产生一个子进程,然后使父进程退出。

  2. 调用setsid()创建一个新会话。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,方法是调用setsid() 使进程成为一个会话组长。setsid() 调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。

    进程组:是一个或多个进程的集合。

    会话周期:会话期是一个或多个进程组的集合。

    禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。

  3. 将当前目录更改为根目录。使用fork() 创建的子进程也继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统不能卸载,因此,把当前工作目录换成其他的路径,如根目录。

  4. 重设文件权限掩码。文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。 由于使用 fork() 函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。 因此,把文件权限掩码设置为0 ,可以大大增强该守护进程的灵活性。 设置文件权限掩码的函数是umask ,通常的使用方法为 umask(0)。

  5. 关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。

我们给出一个实例:实现一个简单的守护进程(每隔2s在/tmp/douya.c中写入一句话)

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <sys/wait.h>  
#include <sys/types.h>  
#include <sys/stat.h>  

#define MAXFILE 65535  

int main(){  
    //第一步:创建进程   
    int pid = fork();  

    if (pid > 0)  
        exit(0);//结束父进程   
    else if (pid < 0){  
        printf("fork error!\n");  
        exit(1);//fork失败,退出   
    }  
    //第二步:子进程成为新的会话组长和进程组长,并与控制终端分离   
    setsid();  
    //第三步:改变工作目录到  
    chdir("/");  
    //第四步:重设文件创建掩模   
    umask(0);  
    //第五步:关闭打开的文件描述符  
    for (int i=0; i<MAXFILE; ++i)   
        close(i);  
    while (1){  
        int fd;  
        if ((fd=open ("/tmp/douya.c", O_CREAT|O_WRONLY|O_APPEND, 0600)) < 0){ 
            perror("open");  
            exit (1) ;  
        }  
        write(fd, "processing...\n", strlen("processing...\n")+1);  
        close(fd);  
        sleep(2);  
    }  
    return 0;  
}  

编译程序后,我们运行程序:

$ ./douya_protected

发现程序一下子就退出了。我们用指令查看一下后台程序

$ ps -ef | grep protected
img

实际上发现我们的程序依然运行着,只不过在后台运行。我们查看"/tmp/douya.c"下的日志,可以看到,进程每隔2s写入"processing...\n"。

img

明白了吧豆芽,所以,如果你想创建一个可以脱离终端运行的进程,可以使用守护进程。其实,linux提供了daemon函数用于创建守护进程,实现原理与上文中介绍的是一样的。这样使用起来就更简单了。

蒋 豆 芽:wonderful!

img

1.11 僵尸进程与孤儿进程

img

隔壁老李:接下来我们就要讲僵尸进程。

蒋 豆 芽:哦?还有这种说法?植物大战僵尸吗?

隔壁老李:(敲脑袋)一天想什么呢?好好听讲行不行!我们逐步讲解,在Linux中,正常情况下,子进程是通过父进程创建的,子进程又创建新的进程,子进程退出后,将由父进程调用wait()或者waitpid()系统调用取得子进程的终止状态。然后就要回收子进程的资源。

然而子进程的结束和父进程的运行是一个异步过程,即父进程无法预测子进程什么时候结束。于是就会产生孤儿进程僵尸进程

  1. 孤儿进程,是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完成状态收集工作。
  2. 僵尸进程,是指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程。

所以两者的区别是:孤儿进程是父进程已退出,子进程未退出;而僵尸进程是父进程未退出,子进程已退出。

隔壁老李:僵尸进程实例如下:

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  

int main() {  
    pid_t pc = fork(); //创建一个子进程  
    if (pc > 0){//父进程  
        printf("in parent process, wait some minutes...\n");  
        sleep(10);  
        printf("after waiting, parent process exits!\n");  
    }  
    else if (pc == 0){  
        //子进程退出,父进程没有调用wait()或waitpid()系统调用  
        //子进程将成为一个僵尸进程  
        printf ("in child process, let it exist!\n");  
        exit(0);  
    }  
    return 0;  
} 

我们通过fork创建子进程,先让父进程等待10s钟,同时让子进程退出,而父进程没有调用wait()或waitpid()系统调用,使得子进程变为僵尸进程。

在程序运行中,我们通过指令查看进程状态:

$ ps aux | grep -w 'Z'  
img

可以清楚地看到,出现了一个僵尸进程。当我们的父进程10s等待完毕后,再次查看

img

僵尸进程没有了。因为父进程退出后,这个僵尸进程已成为孤儿进程,由init进程管理,而init进程会周期性地调用wait系统调用来清除僵尸进程。

蒋 豆 芽:原来是这样啊,有点意思哈!

隔壁老李:那豆芽,这里介绍了僵尸进程的概念。而在实际应用中,如果系统总是产生大量僵尸进程而得不到回收,就会占用大量的系统资源,当然有害于我们系统的正常运行。所以我们要及时回收我们的进程资源。

img

1.12 wait()和waitpid()系统调用

img

蒋 豆 芽:那应该怎么回收呢?

隔壁老李:我们前面说了,可以使用wait()或waitpid()系统调用,进程一旦调用了wait函数,就立即阻塞自己本身,然后由wait函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞,直到有一个出现为止。函数原型如下:

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

pid_t wait(int* status); 

子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一起返回。如果不需要结束状态值,则参数status可以设成 NULL。

我们来看个实例:

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
#include<sys/types.h>  
#include<sys/wait.h>  

int main() {  
    pid_t pc = fork(); //创建一个子进程  
    if (pc > 0){//父进程  
        printf("in parent process\n");  
        //阻塞在这里,等待子进程退出  
        pid_t pchild = wait(NULL);  
        printf("catch a child process pid of %d\n", pchild);  
    }  
    else if (pc == 0){  
        printf ("in child process, PID: %u, PPID: %u\n", getpid(), getppid()); 
        exit(0);//子进程退出  
    }  
    else {  

        printf("fork error!\n");  
        exit(-1);  
    }  
    return 0;  
}  

运行结果如下:

in parent process
in child process, PID: 2812, PPID: 2811
catch a child process pid of 2812   

从结果可以看到,我们成功捕获了子进程的PID,最后子进程将销毁,资源得到释放。

隔壁老李waitpid()功能与wait相同,但可指定pid进程清理,可以不阻塞。原型如下:

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

pid_t waitpid(pid_t pid, int *status, in options);  
//成功:返回清理掉的子进程ID;失败:-1(无子进程)  
  1. 特殊参数和返回情况,参数pid:

    > 0回收指定ID的子进程

    -1回收任意子进程(相当于wait)

    0回收和当前调用waitpid一个组的所有子进程

    < -1回收指定进程组内的任意子进程

  2. 参数status:

    子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一起返回。如果不需要结束状态值,则参数status可以设成 NULL。

  3. 参数options:

    Options=WNOHANG时,如果使用了WNOHANG(wait no hung)参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。

    Options=WUNTRACED时,则子进程进入暂停则马上返回,但结束状态不予以理会。

    如果不想使用,Options=0

注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

看一个实例:

#include <sys/types.h>  
#include <sys/wait.h>  
#include <unistd.h>  
#include <stdio.h>  
#include <stdlib.h>  

int main(void){  
    pid_t pid = fork();  

    if (pid < 0) {  
        perror("fork error!\n");  
        exit(-1);  
    }  
    if (pid == 0) {  
        for (int i=4; i>0; --i) {  
            printf("This is the child\n");  
            sleep(2);  
        }  
        exit(32);  
    }   

    else {  
        int stat_val;  
        waitpid(pid, &stat_val, 0);//0阻塞  
        if (stat_val)  //stat_val为传出参数  
            printf("Child exited with code %d\n", stat_val);  
        else if (stat_val)  
            printf("Child terminated abnormally, signal %d\n", stat_val);  
    }  
    return 0;  
}  

运行结果如下:

This is the child
This is the child
This is the child
This is the child
Child exited with code 8192

子进程打印了4次信息后被回收,waitpid成功捕获了子进程,结束状态也一并打印出来了。

隔壁老李:通过wait()或waitpid()系统调用,我们就实现了及时回收子进程的目标。

蒋 豆 芽:嗯嗯,是的。

隔壁老李:豆芽,还有一种办法,我们上一节才学过,你猜猜?

蒋 豆 芽:emmmm,我想起来了,可以通过kill命令终止进程。

隔壁老李:没错!发送指定的信号到相应进程。可以先打开终端并输入下面命令:

$ ps aux | grep Z

会列出进程表中所有僵尸进程的详细内容。

然后输入命令:

$ kill -s SIGCHLD pid(父进程pid)

这样子进程退出后,父进程就会收到信号了。

或者可以强制杀死父进程:

$ kill -9 pid(父进程pid)

这样父进程退出后,这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完成状态收集工作。

隔壁老李:累死了,这节先讲到这里,我们下一节讲进程的通信方式。

蒋 豆 芽:好,多谢你,老李!

img

故事完

img

参考文献

[1] 徐晓鑫. 后台开发:核心技术与应用实践[M].北京:机械工业出版社,2016.

[2] 安德鲁S.塔嫩鲍姆,赫伯特·博斯. 现代操作系统 [M]. 北京:机械工业出版社,2017.7

[3] 蒋豆芽. C++后端面试知识点大全(春秋招必备)[M]. 长沙:豆芽出版社,2020.

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

全部评论
有些同学没有操作系统的基础,看着似乎有点吃力,我的文章就是帮助没有基础的同学。大家一定要看两遍,有点知识点前面提到了,而后面才有详细解析,所以看两遍非常合适。
4 回复 分享
发布于 2021-04-01 20:45
大家一定要仔细看,我文章没有太多废话,比较精炼,对知识点的领悟往往就藏在这些精炼的语句中。
3 回复 分享
发布于 2021-04-01 20:46
把我的文章看完了,基本就有相应的知识体系大概轮廓了,如果大家心里还意犹未尽的话,就这可以对照我文章的内容再看看书,这样效率就很高,不至于抓瞎。
3 回复 分享
发布于 2021-04-01 20:46
去年看了黑马的Linux系统编程课程,这会看这个加深回忆挺好的。 可是豆芽啊!被放养的十八线小硕不知毕设如何开题,找不到导师要的创新点! 太难了!太难了!太难了呀!😪😪😪😪
1 回复 分享
发布于 2021-04-21 14:31
请问一下豆芽,已经订阅了这个秋招打怪专刊,还需要订阅16.9那个专栏吗,那个专栏里的面试题和这里的一样吗
1 回复 分享
发布于 2021-07-15 16:25
豆芽,c++后端秋招找工作,有一点基础,应该订阅这个还是订阅另一个专栏啊
点赞 回复 分享
发布于 2021-06-25 20:40
想问问豆芽,操作系统进程这些函数,fork,wait像这些,是不是需要学一下linux系统编程才能深入理解,之前只是看过考研书上的操作系统的内容,这些函数都没涉及过,所以有点困惑
点赞 回复 分享
发布于 2021-08-11 21:59
实现守护进程那个编程实例中,关闭父进程继承过来的文件描述符为什么 for循环到MAXFILE呀?
点赞 回复 分享
发布于 2022-03-08 10:47

相关推荐

评论
点赞
收藏
分享
牛客网
牛客企业服务