操作系统面试高频(进程)

进程

1 进程的定义⭐⭐⭐⭐⭐

进程是指在计算机中执行的一个程序。每个进程都有自己的内存空间和系统资源,如文件、端口、线程等。进程是计算机操作系统中的一个基本概念是操作系统进行资源分配和调度的最小单位每个进程都有一个唯一的标识符(进程号),可以在操作系统中进行管理和控制

在操作系统中,每个进程都有自己的地址空间和指令和数据,这个地址空间通常由内核进行分配和管理。在运行时,进程可以创建和销毁多个线程,每个线程可以在进程的地址空间中独立运行。

每个进程可分为以下几个部分:

程序代码区:包含执行程序的指令代码

数据区:包含程序执行时所需的数据

:动态分配的内存空间

:运行时函数调用、返回和局部变量存储的空间

操作系统通过进程控制块(Process Control Block,PCB)来记录和管理每个进程的状态、所有权等信息,每个进程的PCB包含进程标识符、进程状态、CPU寄存器状态、内存分配状态、打开文件状态信息等。操作系统可以通过PCB来进行进程的调度和管理,以保证计算机系统的高效运行

2 进程的特征⭐⭐⭐⭐⭐

进程具有以下特征:

动态性:进程是动态产生和消失的,系统中可以同时存在多个进程,新的进程可以随时被创建,而已有的进程也可以随时被销毁。

独立性:每个进程都有自己的内存空间和系统资源,进程之间不会相互干扰。

并发性:多个进程可以同时运行,计算机系统可以快速地进行多任务处理,提高效率,有利于实现多人共享系统资源。

异步性:不同进程的执行速度不同,各个进程之间的执行顺序和时间不受约束,执行顺序和速度都是不可预知的。

独立的地址空间:每个进程有自己的地址空间,进程之间的数据是相互隔离的,提高了计算机系统的安全性。

同步与通信:不同的进程之间可以进行通信,可以利用操作系统提供的信号量、共享内存、管道等机制来实现进程间的同步和通信。

共享与互斥:在进程并发执行时,可能会有多个进程要对同一资源进行访问,通过锁机制实现资源的互斥访问,避免了资源冲突和死锁问题。

3 进程的五种基本状态及转换⭐⭐⭐⭐⭐

进程有五种基本状态:创建状态、就绪状态、运行状态、阻塞状态和终止状态

创建状态:当操作系统接受到用户进程创建的请求时,会为进程分配资源,并将进程的PCB加入到系统的进程队列中。

就绪状态:当进程具备运行条件时,它被加入就绪队列中,等待操作系统为其分配CPU执行时间。

运行状态:当进程获得CPU执行权后,进程开始运行,执行其指令,直到结束或者进入阻塞状态。

阻塞状态:当进程在运行时需要等待其它的事件发生,比如等待I/O的完成,或者等待系统调用的结果,进程会从运行状态转换到阻塞状态。

终止状态:当进程完成其任务或者因为异常情况终止时,进程会从任何状态转移到终止状态,回收并释放系统资源。

进程状态之间的转换如下:

从创建状态到就绪状态:系统为进程分配资源后,进程进入就绪状态,等待系统分配CPU执行时间。

从就绪状态到运行状态:当进程获得CPU执行资格时,进程进入运行状态,开始执行。

从运行状态到就绪状态:当进程执行完其指令后, 或者因为时间片用完而被中断时,进程从运行状态转换到就绪状态。

从运行状态到阻塞状态:当一个进程在执行中需要等待其它事件的发生,比如等待I/O完成时,进程会从运行状态转换为阻塞状态,等待事件完成。

从阻塞状态到就绪状态:当一个进程等待的事件完成时,它就转移到就绪状态,等待系统重新为其分配CPU执行时间。

从运行状态到终止状态:当进程执行完成或者因为异常情况终止时,它就转移到终止状态,系统回收并释放该进程所占有的资源。

4 创建进程的方式?⭐⭐⭐⭐⭐

在 Linux 系统中,创建进程主要有以下几种方式:

使用 fork() 函数

  • 原理fork() 是创建新进程的基础系统调用。调用 fork() 时,操作系统会复制当前进程(父进程),生成一个几乎完全相同的子进程。子进程会获得父进程数据空间、堆和栈的副本,且与父进程共享代码段。
  • 返回值:在父进程中,fork() 返回子进程的进程 ID(一个正整数);在子进程中,返回 0;若出错则返回 -1。通过返回值,父进程和子进程可区分彼此并执行不同的代码逻辑。
  • 示例代码
#include <stdio.h>
#include <unistd.h>

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

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程代码
        printf("This is the child process, pid = %d\n", getpid());
    } else {
        // 父进程代码
        printf("This is the parent process, child pid = %d\n", pid);
    }

    return 0;
}

fork() 与 exec() 系列函数配合

  • 原理fork() 创建子进程后,子进程可调用 exec() 系列函数来执行新的程序。调用 exec() 时,子进程的内存空间会被新程序替换,开始执行新程序的代码。
  • 常见函数:包括 execl()execv()execle()execve()execlp()execvp() 等,这些函数在参数传递方式和查找可执行文件的路径上存在差异。
  • 示例代码
#include <stdio.h>
#include <unistd.h>

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

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程执行新程序
        if (execl("/bin/ls", "ls", "-l", NULL) == -1) {
            perror("execl");
            return 1;
        }
    } else {
        // 父进程等待子进程结束
        wait(NULL);
        printf("Child process finished.\n");
    }

    return 0;
}

使用 vfork() 函数

  • 原理vfork() 也是用于创建新进程的系统调用,与 fork() 类似,但 vfork() 创建的子进程会共享父进程的地址空间,直到子进程调用 exec() 系列函数或 _exit() 退出。
  • 特点vfork() 保证子进程先运行,在它调用 exec() 或 _exit() 之后父进程才可能被调度运行。
  • 示例代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = vfork();

    if (pid < 0) {
        perror("vfork");
        return 1;
    } else if (pid == 0) {
        // 子进程代码
        printf("This is the child process, pid = %d\n", getpid());
        _exit(0);
    } else {
        // 父进程代码
        printf("This is the parent process, child pid = %d\n", pid);
    }

    return 0;
}

使用 system() 函数

  • 原理system() 函数可在程序中执行一个 shell 命令。它内部会调用 fork() 创建子进程,然后在子进程中调用 exec() 来执行指定的 shell 命令。
  • 优点:使用简单,适合执行一些简单的系统命令。
  • 示例代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int status = system("ls -l");
    if (status == -1) {
        perror("system");
        return 1;
    }
    return 0;
}

5 什么时候用进程?⭐⭐⭐⭐⭐

在操作系统中,进程是资源分配和调度的基本单位。以下是适合使用进程的常见场景及简介:

1. 需要资源隔离时

  • 场景:多个任务需要独立的内存空间、文件描述符等资源,避免相互干扰。
  • 例子:浏览器中每个标签页作为独立进程,一个页面崩溃不影响其他页面。

2. 并行处理任务

  • 场景:需要充分利用多核 CPU,或处理 CPU 密集型任务(如视频渲染、科学计算)。
  • 特点:进程间通过消息传递(如管道、Socket)通信,适合无共享数据的并行任务。

3. 运行独立程序

  • 场景:通过 exec() 系列函数启动外部程序(如 Shell 命令、第三方工具)。
  • 例子:在 C 程序中调用 system("ls -l") 执行文件列表命令。

4. 需要高可靠性和容错性

  • 场景:某个任务崩溃时,不影响其他任务的运行。
  • 例子:Web 服务器的多进程模型,单个进程故障后可自动重启。

5. 守护进程与服务程序

  • 场景:长期运行的后台服务(如数据库、Web 服务器)。
  • 特点:独立于终端,可在系统启动时自动运行。

与线程的对比

  • 进程:资源隔离性强,适合任务独立性高的场景,但创建开销大。
  • 线程:轻量级,共享进程资源,适合任务协作频繁的场景(如 I/O 密集型服务)。

总结

选择进程的核心原则是任务独立性资源隔离。当任务需要独立运行、避免相互影响,或需要充分利用多核性能时,进程是更合适的选择。

6 进程控制⭐⭐⭐⭐

进程控制指的是操作系统通过对进程的调度、调度策略、进程优先级等方式来控制进程的执行。进程控制由操作系统进行,是操作系统对进程状态的监控和管理。具体的控制方式包括以下几种:

进程调度:指操作系统在多个就绪进程中选取一个进程分配CPU执行时间的过程。进程调度的目的是提高CPU的利用率和系统的响应速度。

进程同步:多个进程之间为了协作完成某一任务,必须要进行同步。操作系统提供了各种机制,如信号量、互斥锁、条件变量等来实现进程间的同步。

进程通信:进程通信是指多个进程之间交换信息和共享数据的过程。进程通信是通过操作系统提供的IPC机制来实现的,如管道、消息队列、共享内存、Socket等。

进程安全:进程安全指保证进程能够正确、合法的执行,并防止进程间相互干扰和冲突的一组机制。进程安全是操作系统对进程进行的重要控制之一,其技术手段包括:鉴权、进程隔离、进程权限控制等。

死锁处理:在操作系统中,进程之间可能会因为资源的竞争和协作等原因产生死锁。操作系统提供了死锁预防和死锁解除的机制,如银行家算法、超时机制、资源剥夺等方式来避免和解除死锁。

7 进程同步方式⭐⭐⭐⭐⭐

在多进程操作系统中,由于多个进程都需要共享系统资源,同时对这些资源进行访问可能会产生冲突,因此需要进行同步与互斥。常见的进程同步方式包括:

信号量: 信号量是一个整形变量,可以用来控制多个进程对共享资源的访问。在信号量机制中,对共享资源进行互斥访问的进程会对信号量进行P(proberen)操作,而释放共享资源的进程则进行V(verhogen)操作。当信号量为1时,表示共享资源可以被访问,为0时表示已经被占用。

互斥锁:互斥锁是一种二元信号量,只有0和1两种状态,可以用来保证多个进程互斥地访问共享资源。只有获取到锁的进程才能访问共享资源,其他进程需要等待锁的释放。当锁被占用时,其他进程对锁的请求会被阻塞,直到锁被释放。

条件变量:条件变量可以用来实现进程间的同步和通信。条件变量通常与互斥锁一起使用,当共享资源的某个条件未得到满足时,会阻塞等待,当获取到满足条件的资源时,可以通过条件变量通知阻塞的进程获取共享资源。

读写锁: 读写锁是一种特殊的锁,用于在数据读取操作多于写入操作的情况下提高并发性能。具体来说,读写锁允许多个读者访问,但只允许一个写者访问。当写操作正在被执行时,所有读和写操作都将被阻塞。

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

嵌入式/C++面试八股文 文章被收录于专栏

该专栏面向嵌入式开发工程师、C++开发工程师,包括C语言、C++,操作系统,ARM架构、RTOS、Linux基础、Linux驱动、Linux系统移植、计算机网络、数据结构与算法、数电基础、模电基础、5篇面试题目、HR面试常见问题汇总和嵌入式面试简历模板等文章。超全的嵌入式软件工程师面试题目和高频知识点总结! 另外,专栏分为两个部分,大家可以各取所好,为了有更好的阅读体验,后面会持续更新!!!

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务