【C++八股-第15期】操作系统-进程与线程

你的花花是我更新的动力~

提纲:

👉 八股:

  1. 你了解共享内存么?常见的同步机制有哪些?

  2. 介绍下进程五种状态及状态间转换关系

  3. Linux中如何创建子进程

  4. 写时复制有什么用?原理是什么?

  5. 守护进程是什么?如何创建守护进程

  6. 介绍一下孤儿进程和僵尸进程?如何解决僵尸进程?

  7. 介绍常见的进程间通信(IPC)

1. 你了解共享内存么?常见的同步机制有哪些?

定义及特点

共享内存 (Shared Memory)是一种高效的进程间通信(IPC)机制,它允许多个进程访问同一块内存区域,以便共享数据。

共享内存是 最快 的IPC方式之一,因为数据不需要在进程间复制,而是直接在共享内存区域进行读写操作。

一个进程向共享的内存区域写入数据,其他共享这个内存区域的进程可以立即看到这些内容。

同步问题 :使用共享内存时,需要注意多个进程之间对同一存储区访问的互斥问题。

如果一个进程正在向共享内存区写入数据,则在它完成此操作之前,其他进程不应读取或写入这些数据,以避免数据竞争和不一致性。

常用的同步机制

  • 互斥锁(Mutex) : 用于保护对共享内存的访问,确保同一时间只有一个进程可以对其进行读写操作。

  • 信号量(Semaphore) : 是一个计数器,用于控制对共享资源的访问数量,适用于需要计数的场景。P操作(递减操作)可以用于阻塞一个进程,V操作(增加操作)可以用于解除阻塞一个进程。

  • 管程 :一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。

  • 消息队列 :消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。

  • 条件变量(Condition Variable) : 用于线程间的协调,适用于需要等待某个条件满足的场景。

   

   

2. 介绍进程五种状态及状态间转换关系

进程状态: 新建就绪 运行阻塞终止

  • 新建(New):

    • 需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配
  • 就绪(Ready)

    • 进程已创建并且准备好执行,但尚未被分配到CPU,未获得处理器资源
  • 运行(Running)

    • 获取处理器资源,被系统调度。当调度器选择该进程执行时,状态转换为运行(Running)。
    • 若进程时间片用完或被抢占,则状态转换为就绪(Ready)
  • 阻塞(Blocked)

    • 进程在等待某个事件(如I/O操作、信号等)完成,暂时无法执行
  • 终止(Terminated)

    • 进程已完成执行或被终止。

图片来源于网路

图片来源于网路

   

   

3. Linux中如何创建子进程

在 Linux 中,创建子进程通常通过 fork 系统调用实现

  • 父进程fork() 函数返回新创建子进程的PID

  • 子进程fork()函数,调用成功返回0,失败会返回 -1

代码示例

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

int main() {
    pid_t pid;

    // 调用 fork() 创建子进程
    pid = fork();

    if (pid < 0) {
        // 创建子进程失败
        fprintf(stderr, "Fork Failed\n");
        return 1;
    } else if (pid == 0) {
        // 子进程执行的代码
        printf("This is the child process. PID: %d, Parent PID: %d\n", getpid(), getppid());
        // 在这里可以执行子进程特有的任务
    } else {
        // 父进程执行的代码
        printf("This is the parent process. PID: %d, Child PID: %d\n", getpid(), pid);
        // 父进程可以选择等待子进程结束
        wait(NULL);
        printf("Child process complete.\n");
    }

    return 0;
}

   

   

4. 写时复制有什么用?原理是什么?

写时复制 是一种内存管理优化策略,用于高效地管理进程创建和内存使用,可以显著减少内存消耗和提高效率。

写时复制的基本原理

① 进程创建

  • 当一个进程通过 fork() 函数创建子进程时,操作系统并不立即复制父进程的所有内存内容给子进程。相反,它们开始共享相同的内存空间和资源。

② 共享阶段

  • 父进程和子进程在开始阶段共享同一个内存映像,包括代码段、数据段和堆栈段。这意味着他们看到的内存内容是一样的,不需要额外的内存复制开销。

③ 写操作处理

  • 当父进程或者子进程真的准备修改共享内存内的数据时(例如下面例子中的 写入新值 ),操作系统会进行介入

  • 首先,当操作系统检测到你要进行写操作时,操作系统会在写操作实际执行之前,将要修改的内存页面复制一份形成新的独立副本。

  • 之后再进行之前要进行的写操作,此时父子进程修改的就是各自的独立副本了,互不影响

④ 减耗提效原因

  • 因为大部分情况下进程只需要读取数据而不需要修改数据,因此写时复制延迟了内存的实际复制时间,只有在进程真正需要修改数据时才进行,从而达到了节省内存和提高性能的效果。

代码示例:

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

int main() {
    char *shared_memory = (char *) malloc(1024);  // 分配共享内存

    strcpy(shared_memory, "Initial data");

    pid_t pid = fork();  // 创建子进程

    if (pid == -1) {
        fprintf(stderr, "Fork failed\n");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process: shared_memory = %s\n", shared_memory);
        strcpy(shared_memory, "Child's new data");
        printf("Child process: shared_memory after write = %s\n", shared_memory);
    } else {
        // 父进程
        wait(NULL);  // 等待子进程结束
        printf("Parent process: shared_memory = %s\n", shared_memory);
    }

    free(shared_memory);  // 释放共享内存
    return 0;
}
// 输出:
Child process: shared_memory = Initial data
Child process: shared_memory after write = Child's new data
Parent process: shared_memory = Initial data  // 子进程修改并不会影响父进程

   

   

5. 守护进程是什么?如何创建守护进程

守护进程(Daemon)是在计算机系统后台运行的一种特殊类型的长期进程,通常独立于控制终端,并且没有用户交互界面。它们通常用于执行系统级别的任务、服务和管理,如网络服务、日志处理等。

创建守护进程的步骤

造子杀父 - 创建子进程,终止父进程:

  • 父进程通过 fork() 创建一个子进程,然后立即退出,这样子进程就成为孤儿进程,这一步确保了守护进程不会成为终端的会话首进程。
pid_t pid = fork();
if (pid < 0) {
    // 创建子进程失败
    exit(EXIT_FAILURE);
} else if (pid > 0) {
    // 父进程退出
    exit(EXIT_SUCCESS);
}

   

② 调用 setsid() 创建新会话:

  • 子进程调用 setsid() 函数创建一个新的会话,并成为该会话的首进程和组长进程。这一步确保守护进程不再有控制终端,从而独立于任何终端会话。
if (setsid() < 0) {
    // 创建新会话失败
    exit(EXIT_FAILURE);
}

   

③ 将当前工作目录更改为根目录:

  • 为了避免守护进程占用挂载的文件系统,通常将当前工作目录更改为根目录。
if (chdir("/") < 0) {
    // 切换工作目录失败
    exit(EXIT_FAILURE);
}

   

④ 重设文件权限掩码:

  • 守护进程需要确保不会创建不必要的文件或者过于开放的文件权限,因此需要重设文件创建掩码。
umask(0);

   

⑤ 关闭文件描述符:

  • 守护进程不再需要继承自父进程的文件描述符,因此需要关闭所有已打开的文件描述符。
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

   

   

6. 介绍一下孤儿进程和僵尸进程?如何解决僵尸进程?

孤儿进程

父进程终止了,但是自己没有终止,还活着就成孤儿了

  • 孤儿进程的处理:

    • 当父进程终止时,孤儿进程会被重新分配给 init(PID 1) 进程。init 进程会负责对这些孤儿进程进行善后处理。

    • init 进程会周期性地调用 wait()waitpid() 函数来收集这些孤儿进程的退出状态,避免它们成为僵尸进程。

僵尸进程

自己终止了,但是父进程不知道你终止了,你就成僵尸了

僵尸进程 是指一个已经终止但其退出状态信息尚未被父进程读取的进程。尽管它已经停止运行,但在系统的进程表中仍然保留一个条目,以保存它的退出状态信息,直到父进程读取它,这会占用系统资源。

僵尸进程的产生:
  • 子进程终止后,它会向父进程发送一个 SIGCHLD 信号。

  • 父进程需要调用 wait()waitpid() 函数来读取子进程的退出状态。

  • 如果父进程没有读取到子进程的退出状态,子进程的条目会一直保留在进程表中,形成僵尸进程。

   

解决僵尸进程的方法:
  • 父进程调用 wait()waitpid()

    • 父进程在适当的时机调用 wait()waitpid() 函数来收集所有已终止子进程的退出状态。
  • 使用 SIGCHLD 信号处理程序:

    • 父进程可以注册一个 SIGCHLD 信号处理程序,当子进程终止时,这个信号处理程序会被调用,父进程在处理程序中调用 wait()waitpid() 来收集子进程的退出状态。
  • 使用 wait() 的自动化处理: - 在父进程的主循环中定期调用 wait()waitpid(),以确保及时处理已终止的子进程。

   

   

7. 介绍常见的进程间通信(IPC)

常见的:管道(Pipe)消息队列(Message Queue)信号量(Semaphore)信号(Signal)共享内存(Shared Memory)套接字(Socket)

① 管道(Pipes)

管道是一种最基本的进程间通信方式,提供了一个单向的数据流,父子进程之间常用。

  • 匿名管道

    • 适用于具有亲缘关系的进程(如父子进程)。使用 pipe() 函数创建。
  • 命名管道(FIFO)

    • 适用于不具有亲缘关系的进程。使用 mkfifo() 函数创建。

   

② 消息队列(Message Queues)

消息队列允许进程以消息为单位进行通信,支持消息的优先级。

消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。

使用 msgget()msgsnd()msgrcv() 等函数操作。

   

③ 信号量(Semaphore)

**信号量(Semaphore)**用于控制多个进程对共享资源的访问。

信号量的基本思想是通过信号量变量对进程进行控制,确保同一时间只有一定数量的进程能够访问共享资源。

信号量有两个基本操作:

  • P操作(等待,wait):将信号量的值减1。如果信号量的值已经是0,进程将阻塞,直到信号量的值大于0。
  • V操作(信号,signal):将信号量的值加1。如果有进程因为信号量的值为0而阻塞,将唤醒一个进程。

   

使用信号量的步骤

  • 创建信号量:使用semget函数创建或获取一个信号量。
  • 初始化信号量:使用semctl函数初始化信号量的值。
  • P操作:使用semop函数进行P操作。
  • V操作:使用semop函数进行V操作。
  • 删除信号量:使用semctl函数删除信号量。

   

④ 信号(Signal)

信号 是一种用于通知进程某个事件发生的机制,可以异步地通知进程进行相应的处理。

#include <signal.h>
#include <unistd.h>
#include <iostream>

void signal_handler(int signum) {
    std::cout << "Received signal: " << signum << std::endl;
}

int main() {
    signal(SIGUSR1, signal_handler); // 注册信号处理程序

    pid_t pid = fork();

    if (pid == 0) {
        // 子进程
        sleep(2); // 等待父进程设置信号处理程序
        kill(getppid(), SIGUSR1); // 向父进程发送信号
    } else {
        // 父进程
        pause(); // 等待信号
    }

    return 0;
}

   

⑤ 共享内存(Shared Memory)

共享内存允许多个进程直接访问同一块内存区域,是最快的进程间通信方式之一,但需要同步机制来避免竞争。

   

⑥ 套接字(Socket)

套接字提供了在网络和本地进程间通信的机制。支持TCP和UDP协议。套接字不仅用于网络通信,也可以用于同一主机上不同进程之间的通信。

   

   

全部评论

相关推荐

CODERKEY:你这简历写了一堆,一眼看下来找不到任何重点,任何有价值含金量的内容。还有,你这校外实习经历跟小学生春游一样,具体公司具体岗位实习时间是一个没有。
点赞 评论 收藏
分享
3 11 评论
分享
牛客网
牛客企业服务