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

