Linux进程控制

张开发
2026/5/6 13:02:47 15 分钟阅读

分享文章

Linux进程控制
一、线程的优缺点线程优点创建代价更低创建新线程的代价比创建新进程小得多系统开销显著更低。线程上下文切换效率更高与进程切换相比线程切换需要操作系统完成的工作更少。核心区别在于线程切换时虚拟内存空间保持不变而进程切换会更换虚拟地址空间切换过程中均需要保存恢复寄存器内容且不会像进程切换那样刷新 TLB快表不会导致内存访问效率大幅下降同时也能减少对硬件缓存的扰乱避免缓存失效带来的性能损耗。在 CPU 中存在cache 硬件用于存储频繁使用的数据或指令当进程切换时cache 中的缓冲全部失效需要清空重新载入而线程之间共享地址空间cache 内容依然有效不需要对 cache 做大量失效处理因此效率更高。占用系统资源更少线程共享进程的地址空间及各类资源自身占用的资源远少于独立进程。充分利用多核处理器并行能力能高效利用多处理器的并行特性提升程序整体执行效率。提升程序执行流畅度程序在等待慢速 I/O 操作时其他线程可继续执行计算任务避免整体阻塞。适配计算密集型应用针对计算密集型场景可将计算任务拆分到多个线程在多核系统上并行运行。优化 I/O 密集型应用性能对于 I/O 密集型应用可让多个线程分别等待不同 I/O 操作实现 I/O 操作重叠执行大幅提升性能线程缺点性能损失很少被外部事件阻塞的计算密集型线程通常难以与其他线程共用同一个处理器。如果这类线程的数量超过系统可用处理器数量就会产生较大性能损失主要是增加了额外的线程同步和调度开销而系统可用的计算资源并没有变化导致整体效率降低。健壮性降低编写多线程程序需要更全面、细致的设计由于线程之间共享资源再加上系统调度时序上的细微差异或者因错误共享了不该共享的变量很容易引发程序问题。线程之间缺乏有效的隔离保护一个线程出错或数据异常很容易影响整个进程。缺乏访问控制进程是操作系统进行访问控制的基本单位而同一进程内的线程之间共享资源、没有独立的访问控制权限。在一个线程中调用某些系统函数其产生的影响会作用于整个进程无法实现线程级别的独立控制与隔离。二、线程资源的共享、独占进程是资源分配的基本单位线程是具体调度的基本单位。进程具有独立性一个进程中的数据代码一般默认不可以被其它进程获取如共享内存。线程缺乏独立性在同一个进程中线程共享同一块地址空间线程之间会共享数据如文件描述符表、每种信号的处理方式包括 SIG_IGN、SIG_DFL 或自定义的信号处理函数、当前工作目录、用户 ID 和组 ID但是线程之间还是会保留自己的“私有”数据如线程 ID、一组寄存器用于存储线程的上下文数据、独立的栈空间、专属的 errno 变量、独立的信号屏蔽字、自身的调度优先级。正因为线程共享进程的地址空间和核心资源一旦某个线程触发致命错误如野指针、内存越界、非法指令会触发进程级信号如SIGSEGV、SIGILL导致整个进程终止——无论其他线程是否正常执行都会被强制打断本质是进程整体中断而非仅单个线程终止。进程与线程的关系如下三、Linux线程控制POSIX 线程库pthread之间我们简单介绍了一下POSIX库其作为跨平台的Linux线程库包含了Linux核心线程相关函数底层还是clone函数封装头文件是pthread.h使用的时候记得链接posix库功能分类接口函数作用说明线程创建 / 退出pthread_create()创建新线程可指定线程执行函数、私有栈大小等对应线程私有栈 / IDpthread_exit()线程主动退出释放自身资源pthread_join()等待指定线程结束线程调度相关线程私有资源pthread_self()获取当前线程 ID对应线程私有 IDpthread_key_create()创建线程私有数据TSD实现线程级别的 errno 等私有变量对应私有 errno线程同步pthread_mutex_t互斥锁解决线程共享资源的竞态条件对应线程共享资源的安全访问pthread_cond_t条件变量实现线程间的等待 / 唤醒线程属性pthread_attr_t设置线程属性如调度优先级、分离状态对应线程私有调度优先级信号屏蔽pthread_sigmask()设置线程的信号屏蔽字对应线程私有信号屏蔽字创建线程pthread_create()参数类型作用threadpthread_t*输出参数成功创建后会将新线程的 ID 写入该指针指向的内存pthread_t参数类型在Linux下是unsigned long类型attrconst pthread_attr_t*线程属性指针可设置栈大小、分离状态、调度策略等传NULL表示使用默认属性start_routinevoid* (*)(void*)线程入口函数指针新线程启动后会执行这个函数argvoid*传递给start_routine的参数若不需要传参可传NULL成功返回0失败返回非0错误码。#include stdio.h #include unistd.h #include pthread.h #include iostream void *Text(void *i) { int *count static_castint *(i); sleep(1); std::cout 这里是新线程 *count std::endl; delete count; return nullptr; } int main() { pthread_t id[4]; for (int i 1; i 5; i) { sleep(1); std::cout 这里是main线程 std::endl; int *p new int(i); void *b static_castvoid *(p); int n pthread_create(id (i - 1), nullptr, Text, b); if (n ! 0) { std::cout pthread_create failed std::endl; delete p; } } sleep(4); std::cout main线程退出 std::endl; return 0; }代码中的参数对应函数原型参数具体作用id (i - 1)pthread_t *thread1.id是pthread_t id[4]数组2. 线程创建成功后系统会把新线程的 ID 写入这个地址3. 循环中i从 1 到 4因此会依次将 4 个线程的 ID 存入id[0]、id[1]、id[2]、id[3]避免线程 ID 被覆盖。nullptrconst pthread_attr_t *attr表示使用线程的默认属性Textvoid *(*start_routine)(void *)1. 指定新线程创建后要执行的函数是void *Text(void *i)2. 新线程启动后会立刻进入Text函数执行逻辑sleep (1) → 输出数值 → 释放内存 → 返回。bvoid *arg1. 这个参数会被传递给Text函数的形参i让子线程能读取到当前循环的i值1/2/3/42. 用new分配内存保证了每个线程拿到的是独立的数值不会因循环变量地址复用导致数据错误。线程等待pthread_join()跟进程类似子线程退出时也需要父线程处理后事否则就会出现类似僵尸进程的问题。pthread_join函数就是专门处理这种情况的。参数名类型作用threadpthread_t必选参数指定要等待的目标线程 ID由pthread_create()输出的线程标识。retvalvoid **可选参数传NULL不获取线程退出返回值最常用传有效指针接收线程退出时return或pthread_exit()的返回值。#include pthread.h #include stdio.h void* thread_func(void* arg) { // 子线程返回数值 100 return (void*)100; } int main() { pthread_t tid; pthread_create(tid, NULL, thread_func, NULL); void* ret; // 等待子线程并接收返回值 pthread_join(tid, ret); printf(子线程退出返回值%d\n, (int)ret); // 输出100 return 0; }退出码问题在线程控制模块TCB中专门有一个void*类型的变量来记录存储线程的退出信息这也是为什么pthread_join第二个参数是void**类型的缘故第二个参数本质就是指向并获取void*类型的退出码。内核 TCB: void* exit_code (void*)100; // 存储线程退出码 用户层: void* ret; // 用来接收退出码的变量 pthread_join: 接收 ret即 void** 类型将 exit_code 的值写入 retvoid*问题void*类型可以转化任何类型这样一来退出码除了常见的整数也可以是类等其它更灵活的类型。#include pthread.h #include iostream using namespace std; struct ExitMsg { int code; const char* msg; }; void* thread_int(void* arg) { return (void*)100; } // 返回结构体退出码 void* thread_struct(void* arg) { // 必须用堆内存new避免局部变量销毁 ExitMsg* res new ExitMsg{200, 执行完成}; // 用void*承载结构体地址 return (void*)res; } int main() { pthread_t tid1, tid2; void* ret; // 接收退出码的通用指针 // 演示1获取整数退出码 pthread_create(tid1, nullptr, thread_int, nullptr); pthread_join(tid1, ret); int int_code (int)ret; cout 整数退出码 int_code endl; // 演示2获取结构体退出码 pthread_create(tid2, nullptr, thread_struct, nullptr); pthread_join(tid2, ret); ExitMsg* struct_res (ExitMsg*)ret; cout 结构体退出码 struct_res-code 信息 struct_res-msg endl; // 输出200 执行完成 // 释放堆内存避免泄漏 delete struct_res; return 0; }线程终止若想要仅终止进程内的某个线程、而不终止整个进程可采用以下三种方法1、从线程函数中执行 return 退出这是子线程正常退出的常用方式但该方法对主线程不适用 —— 主线程main 函数执行 return等价于调用 exit ()会直接终止整个进程。2、线程调用 pthread_exit () 终止自身线程在执行过程中可主动调用该函数来终止自己仅影响当前线程不会波及进程或其他线程。void* thread_func(void* arg) { cout 子线程执行中准备提前退出 endl; sleep(1); // 主动调用pthread_exit终止自身退出码200 pthread_exit((void*)200); // 以下代码不会执行 cout 子线程这行代码永远不会输出 endl; }3、通过 pthread_cancel () 终止同进程的其他线程一个线程可借助该函数指定目标线程的 ID强制终止同一进程中的另一个线程。参数thread为要强制终止的目标线程 ID由 pthread_create 生成返回值成功返回 0失败返回非 0 错误码如线程不存在。// 子线程无限循环等待被强制终止 void* thread_func(void* arg) { while (true) { cout 子线程正在运行等待被取消 endl; sleep(1); } } int main() { pthread_t tid; pthread_create(tid, nullptr, thread_func, nullptr); // 让子线程运行3秒后强制终止它 sleep(3); cout 主线程准备强制终止子线程 endl; pthread_cancel(tid); // 主线程终止子线程 // 等待子线程退出获取退出码 void* ret; pthread_join(tid, ret); // 被cancel的线程退出码固定为PTHREAD_CANCELED即(void*)-1 cout 主线程子线程已被cancel退出码 (long long)ret endl; cout 主线程进程仍在运行 endl; return 0; }线程分离 pthread_detach当父线程会因为需要无用等待子线程而造成资源浪费时我们可以将两者之间进行分离操作这样一来子线程在退出时就不需要父线程可以直接由OS释放资源。参数thread要设置为分离态的目标线程 ID返回值成功返回 0失败返回非 0 错误码如线程已退出、线程已分离线程分离仅改变线程资源的回收逻辑并非真正 “脱离” 进程或其他线程分离后的线程仍与进程内所有线程共享同一地址空间全局变量、堆内存、文件描述符等因此一旦分离线程出现致命错误如野指针、内存越界会触发进程级信号导致整个进程崩溃进程内所有线程无论是否分离都会随之终止。四、线程IDpthread_t类型1. 类型本质Linux 系统中pthread_t本质为unsigned long类型64 位系统等价于unsigned long long仅在所属进程内唯一是操作线程的核心标识。从实现逻辑来看Linux 下pthread_t并非单纯的无符号整数其数值对应线程控制块TCBstruct pthread结构体在进程虚拟地址空间中的起始内存地址。从图中可以清晰看到每个线程都对应一个独立的struct pthread结构体TCB它和线程局部存储、线程栈一同被pthread动态库管理并通过mmap映射到进程的虚拟地址空间中pthread_t tid、pthread_t tid2这些线程 ID本质就是指向各自struct pthread结构体的指针pthread库正是通过这个 “地址型 ID”快速定位到对应线程的 TCB进而读取 / 修改线程的控制信息如退出码、栈地址、信号屏蔽字等完成pthread_join/pthread_cancel等操作这也是该 ID 仅在进程内唯一的核心原因虚拟内存地址只在当前进程的地址空间中有意义跨进程后地址失效因此线程 ID 无法在进程间使用。2. 获取方式场景方法说明获取当前线程 IDpthread_self()线程内部调用返回自身pthread_t类型 ID获取其他线程 IDpthread_create第一个参数创建线程时新线程 ID 会写入该参数指向的pthread_t变量3. 存储 打印规范存储优先使用pthread_t原生类型也可直接用unsigned long类型捕获示例unsigned long tid (unsigned long)pthread_self();打印用unsigned long存储时可使用%ld格式符无符号长整型打印示例printf(线程ID%ld\n, (unsigned long)pthread_self());64 位系统也可选用unsigned long long存储 %llu格式符打印兼容性更优。4. 比较方式遵循 POSIX 标准使用pthread_equal(tid1, tid2)函数判断两个pthread_t类型的线程 ID 是否相同。5. 核心用途作为pthread_join、pthread_cancel、pthread_detach等线程操作函数的核心参数用于指定目标线程。

更多文章