2.4 操作系统 任务:进程与线程

一、Linux 下的七种进程状态

运行状态 R、浅度睡眠状态(可中断睡眠) S、深度睡眠状态(不可中断睡眠) D、停止状态 T、死亡状态 X、 僵死状态 Z( 进程一般退出的时候,一般其不会立即彻底退出。如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态,这也是为了方便后续父进程读取子进程的相关退出结果。 )、进程跟踪状态 t

二、进程和线程的区别

进程是资源分配和保护的基本单位,拥有独立的地址空间;线程是 程序执行和调度的基本单位,多个线程共享同一个进程的内存和资源。

(1)执行与调度

进程是重量级的。创建、销毁、切换成本高(因为需要切换地址空间,cpu缓存命中率大幅降低、cpu寄存器状态、内核栈也都需要切换)。

线程是轻量级的。创建、销毁、切换成本低(因为在同一个地址空间)。

(2)隔离性与安全性

进程强隔离线程弱隔离,一个线程崩溃,整个进程都会崩溃。

(3)通信

进程:管道、消息队列、共享内存、信号、socket。

线程:直接共享内存,共享全局变量。

(4)执行

进程有程序执行的入口,线程不能独立执行,需依存在程序中。

三、进程和线程的选用

进程:提高安全性、需要独立的地址空间和系统资源、需要隔离任务、提高系统稳定性,不限制开销和效率。

线程:需要高效的通信、轻量级任务(任务较轻且需要频繁的切换)、实时性要求。

四、多进程、多线程的优缺点

多进程:

优点:独立性、安全性、可扩展性(可以将多个进程扩展到不同的机器上)

缺点:进程创建和管理的开销大、通信复杂、切换较慢。

多线程:

优点:轻量级、资源共享、响应较快。

缺点:安全性问题、内存占用(最多不能超过进程分配的内存空间大小)。每个线程都需要独立的栈空间和线程控制块(TCB)。

五、孤儿进程、僵尸进程、守护进程

孤儿进程:子进程好没结束,父进程就结束了,孤儿进程将被1号 systemd 进程收养,不会造成资源的泄露。

僵尸进程:子进程终止,但父进程在其终止后并未对其回收,进程描述符仍然占用进程表条目,但不再执行任何操作,会造成系统资源的泄露,导致系统无法创建新的进程(系统能够使用的进程号是有限的)。

守护进程:守护进程独立于终端在后台运行,不与用户直接交互,通常由 systemd 进程启动,只在用户需要时,才唤醒的进程。

守护进程的实现:1、创建一个子进程;2、父进程结束;2、子进程创建新会话,成为会话领导;3、将工作目录改为根目录;4、将文件权限掩码设置为 0(不会做权限扣除);5、关闭文件描述符(关闭所有从父进程继承的文件描述符(包括标准输入、输出、错误));6、重定向标准流:将标准输入、输出、错误重定向到/dev/null,防止守护进程意外使用终端。

void daemonize() {
    // 1. 创建子进程
    pid_t pid = fork();
    if (pid < 0) {
        std::cerr << "Fork failed!" << std::endl;
        exit(EXIT_FAILURE);
    }
    
    // 2. 父进程退出
    if (pid > 0) {
        exit(EXIT_SUCCESS);
    }

    // 3. 子进程创建新会话,成为会话领导
    if (setsid() < 0) {
        std::cerr << "Failed to create new session!" << std::endl;
        exit(EXIT_FAILURE);
    }

    // 4. 更改工作目录到根目录
    if (chdir("/") < 0) {
        std::cerr << "Failed to change working directory!" << std::endl;
        exit(EXIT_FAILURE);
    }

    // 5. 设置文件权限掩码为0(不屏蔽任何权限位)
    umask(0);

    // 6. 关闭所有打开的文件描述符
    for (int fd = sysconf(_SC_OPEN_MAX); fd >= 0; --fd) {
        close(fd);
    }

    // 重定向标准输入、输出、错误到/dev/null
    int devNull = open("/dev/null", O_RDWR);
    if (devNull == -1) {
        std::cerr << "Failed to open /dev/null!" << std::endl;
        exit(EXIT_FAILURE);
    }

    dup2(devNull, STDIN_FILENO);
    dup2(devNull, STDOUT_FILENO);
    dup2(devNull, STDERR_FILENO);
    close(devNull);
}

int main() {
    daemonize();

    // 守护进程的实际工作代码
    while (true) {
        // 这里可以放置守护进程的主要逻辑
        sleep(1);  // 示例:每秒执行一次
    }

    return EXIT_SUCCESS;
}

六、内核线程与用户线程

内核线程:是运行在内核空间的特殊进程,它没有独立的地址空间,只在内核态运行,由内核直接调度和管理。

用户线程:是运行在用户空间的线程,属于某个特定的用户进程,在进程的地址空间内运行。内核并不知道多线程的存在,处理器分配时间只会分配给所在的用户进程,而由线程库进一步为每个线程分配时间,所以每个线程分配的时间相对较少。

内核线程与用户线程区别:

1、内核线程是内核可感知的,用户线程内核不可感知;

2、用户线程的创建、撤销、调度不需要内核支持;内核线程则需要;

3、用户线程的调度依赖线程库;内核线程调度依赖于系统内核。

4、用户线程只能在用户态下运行,内核线程可以运行在任何状态。

二者优缺点:

内核线程:

优点:当有多个处理机时,一个进程的多个线程可以同时并行执行。

缺点:由内核进行调度。

用户线程:

优点:管理容易,代价小

缺点:多个处理机下,同一个进程中的多个线程,只能在同一个处理机下分时复用。

Linux 创建的线程就是内核管理的线程,不适用上述模型,是可被内核感知,并且可在多核运行。

七、 fork()和 vfork()区别

fork 创建新的子进程,该子进程会进行写时复制。

vfork 创建的子进程不复制父进程的地址空间,而是与父进程共享地址空间,子进程的内存修改直接影响父进程。而且在子进程结束之前,父进程一直阻塞。

八、Server 端监听端口,但还没有客户端连接进来,此时进程处于什么状态?

最普通的Server模型,则处于阻塞状态;如果使用IO复用中 epoll、select 等,则处于运行状态。

九、创建线程池

1、设置一个生产者消费者队列,作为临界资源。

2、初始化 n 个线程,并让其运行起来,加锁(条件变量)去队列取任务运行。

3、当任务队列为空的时候,所有线程阻塞(靠条件变量)。

4、当生产者队列来了一个任务后,先对队列加锁把任务挂在到队列上,然后使用条件变量去通知阻塞中的一个线程

#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>

class ThreadPool {
public:
    // 构造函数,启动指定数量的工作线程
    explicit ThreadPool(size_t threads_num = std::thread::hardware_concurrency()) 
        : stop(false) {
        for (size_t i = 0; i < threads_num; ++i) {
            workers.emplace_back([this] { workerThread(); });
        }
    }

    // 添加无参数、无返回值的任务
    void enqueue(const std::function<void()>& task) {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            if (stop) {
                throw std::runtime_error("Cannot enqueue on stopped ThreadPool");
            }
            tasks.push(task);
        }
        condition.notify_one();
    }

    // 添加普通函数指针任务
    void enqueue(void (*task)()) {
        enqueue(std::function<void()>(task));
    }

    // 析构函数,优雅关闭
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread &worker : workers) {
            worker.join();
        }
    }

private:
    // 工作线程执行逻辑
    void workerThread() {
        while (true) {
            std::function<void()> task;
            {
                std::unique_lock<std::mutex> lock(queue_mutex);
                condition.wait(lock, [this] { 
                    return stop || !tasks.empty(); 
                });

                if (stop && tasks.empty()) {
                    return;
                }

                task = std::move(tasks.front());
                tasks.pop();
            }
            task();
        }
    }

    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

// 示例任务函数
void exampleTask(int id) {
    std::cout << "Task " << id << " started on thread " 
              << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Task " << id << " finished" << std::endl;
}

int main() {
    ThreadPool pool(2);  // 2个工作线程

    // 提交10个任务
    for (int i = 0; i < 10; ++i) {
        // 使用lambda捕获参数
        pool.enqueue([i] { exampleTask(i); });
    }

    // 主线程等待一段时间让任务完成
    std::this_thread::sleep_for(std::chrono::seconds(5));
    return 0;
}
C++/嵌入式开发 秋招面经 文章被收录于专栏

一名985硕,在25年秋招中斩获多个C++/嵌入式开发Offer。本专栏将分享我的面经,涵盖C/C++、操作系统、计算机网络、ARM体系与架构、Linux应用/驱动开发、Qt、通信协议及开发工具链等核心内容。

全部评论

相关推荐

肥肠椒绿:双非本可不就犯天条了,双非本就应该打入无间地狱
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

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