news 2026/10/30 2:42:08

Linux嵌入式学习——线程与线程控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux嵌入式学习——线程与线程控制

一、线程的概念

线程是轻量级的进程,线程属于某个进程。一个进程可以有多个线程。

1. 进程与线程相同点

相同点说明
都能实现并发进程和线程都是并发执行的单位,可同时推进多个任务
都是系统调度/管理的对象都由内核创建、调度、销毁,都有生命周期
都可用于处理耗时任务把耗时操作放到子进程/子线程,避免阻塞主执行流

2. 进程与线程不同点

对比维度进程线程
系统角色系统中最小的资源分配单位系统中最小的执行单位
包含关系包含一个或多个线程,默认有一个主线程属于某个进程,线程之间是平级关系
资源资源独立(各自独立地址空间)共享所属进程的资源
栈区独立地址空间有各自独立的栈区,栈区不共享
稳定性相对稳定,一个进程崩溃不影响其他进程不稳定,多线程中一个线程崩溃,整个进程崩溃
创建开销大,新建进程需创建约 3GB 空间小,只需在进程空间中开辟新栈区(约 8MB)
并发度较低更高
通信/共享资源隔离,需 IPC(管道、共享内存等)直接读写共享变量,方便但需加锁同步
切换开销大(涉及地址空间切换)小(同一地址空间内切换

3. 线程编程的步骤 POSIX

POSIX 是操作系统接口标准,POSIX 线程(pthread) 就是遵循该标准的线程 API。

1> pthread_create 创建 →
2> 线程函数里操作自己的栈区和共享资源 →
3> pthread_join / pthread_detach 回收资源。因为默认是 joinable 状态,线程退出后栈区不会自动释放,必须 join 或 detach,否则资源泄漏。

3.1 什么是POSIX

POSIX=PortableOperatingSystemInterface(可移植操作系统接口),是IEEE 制定的一套标准,规定操作系统应该提供哪些接口(系统调用、库函数、线程 API 等)。

要点说明
POSIX 是“标准”,不是“实现”它只规定“接口长什么样、行为如何”,具体由各系统实现。
谁遵循它Linux、Unix、macOS 基本都遵循;Windows 原生不遵循(但有兼容层)。
线程部分叫 POSIX Threads简称pthread,就是我们用的pthread_create、pthread_join这一套。
好处按 POSIX 写的多线程代码,在 Linux/Unix/macOS 上基本能直接移植。

所以“POSIX 线程编程步骤”= 用pthread 标准 API写多线程的流程。

4. linux中查看进程的相关命令

4.1 ps -eLo pid,ppid,lwp,stat,comm
ps -eLo pid,ppid,lwp,stat,comm
字段含义详细解释
PIDProcess ID进程 ID。代表这个线程属于哪个进程。
PPIDParent Process ID父进程 ID。创建这个进程的上层进程 ID。
LWPLight Weight Process线程 ID(轻量级进程 ID)。
在这个输出里,如果同一个 PID 下出现了多行,且 LWP 不同,就说明这是一个多线程程序。
STATProcess State进程/线程状态(这是这个命令最大的亮点)。详见下方扩展。
COMMCommand命令名称。只显示可执行文件的名字,不显示路径和参数(最长 15 个字符)。

常见基础状态:

R (Running/Runnable):运行中或正在等待 CPU 时间片。

S (Sleeping):可中断的睡眠状态(比如在等网络、等键盘输入、等锁)。最常见的状态。

D (Disk Sleep):不可中断的睡眠状态(通常在等待磁盘 I/O,此时连 kill -9 都杀不掉)。

T (Stopped):暂停状态(比如被 SIGSTOP 信号暂停,或在调试器中被断点卡住)。

Z (Zombie):僵尸状态(进程已经结束,但父进程还没回收它)。

X (Dead):彻底死亡。

常见附加修饰符(跟在基础状态后面):

<:高优先级。

N:低优先级。

l:多线程(如果是进程级别的状态,带 l 说明它有子线程)。

s:会话首进程(Session leader)。

+:位于前台进程组。

4.2 ps -eLf,效果如下:
ps -eLf ps:Process Status,查看进程状态。 -e:代表 Every,显示系统中所有的进程(等同于 -A)。 -L:代表 LWP (Light Weight Process,轻量级进程),加上这个参数就会显示线程信息。 -f:代表 Full,以全格式显示详细信息(包含 UID, PID, PPID 等)。

列名全称 / 含义详细解释
UIDUser ID运行该进程/线程的用户 ID(如图片中的 root)。
PIDProcess ID进程 ID,系统分配给该进程的唯一标识符。
PPIDParent Process ID父进程 ID,创建该进程的上一个进程的 ID(如 PID 为 3 的父进程是 2)。
LWPLight Weight Process轻量级进程 ID,实际上就是线程 ID。
注意:对于单线程进程,LWP 通常等于 PID;对于多线程进程,主线程的 LWP 等于 PID,其他子线程的 LWP 是系统另行分配的编号。
CCPU UtilizationCPU 利用率,表示进程最近使用的 CPU 百分比(偶尔用来参考,精确度不如top)。
NLWPNumber of LWPs线程数量,表示该进程内当前共有多少个线程。
如图都是 1,说明这些都是单线程进程或内核线程;如果是普通的多线程程序,这里会显示 > 1 的数字。
STIMEStart Time启动时间,进程开始运行的时间或日期(图中是 10月16日)。
TTYTeletype / Terminal终端设备,该进程依附的终端。?表示没有控制终端(通常是系统守护进程/内核进程)。
TIMECPU Time该进程累计占用的CPU 总时间(格式为分:秒)。
CMDCommand命令名称,启动该进程的命令或程序名。

二、线程的创建

1. 函数:pthread_create();创建线程

注意回调函数,第三个参数。

pthread_t tid; 定义线程号:用于保存新创建线程的 ID,pthread_join/detach需使用 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); pthread_t *thread 出参:指向 pthread_t 变量的指针。 调用成功后,内核把新线程的 ID 写入 *thread(即 &tid)。 必须传有效地址,不能传 NULL。 const pthread_attr_t *attr 入参:线程属性(栈大小、分离状态、调度策略等)。 传 NULL 表示使用默认属性(可结合、默认栈大小、默认调度)。 需要定制时先 pthread_attr_init 再传入。 void *(*start_routine) (void *) 入参:线程入口函数指针,即线程启动后执行的函数。 原型必须严格是 void *func(void *),参数和返回值都是 void*。 返回值可被 pthread_join 的第二个参数接收。 void *arg 入参:传给 start_routine 的唯一参数,类型为 void*。 需要传多个值时可传结构体指针(注意生命周期/内存管理)。 不需要参数时传 NULL。 返回值:成功返回 0;失败返回错误码(非 errno), 如 EAGAIN(资源不足)、EINVAL(属性非法)、EPERM(无权限)。 注意:pthread 函数返回的是错误码本身,不要用 perror,应 strerror(ret)。

2. 函数:pthread_self();获取线程号

pthread_t tid; 定义变量接收当前线程的 ID (pthread_t 在 Linux 下本质是 unsigned long int) pthread_t pthread_self(void); 返回值: 成功:返回当前线程的线程 ID (pthread_t 类型)。 失败:无。POSIX 标准规定该函数永远成功(因为它只是读取自身的 TCB 数据,无失败可能)。 打印提示:因为 pthread_t 底层通常是 unsigned long int,打印时建议强制转换并搭配 %lu: printf("%lu\n", (unsigned long)pthread_self()); void 入参:void(无参数)。 不需要传任何东西,它只针对“调用它的那个线程”自己生效。

三、线程的退出和回收

1. 线程自行退出

void pthread_exit(void *retval); void *retval 入参:线程退出时的返回状态("临死遗言")。 可以通过强转传整数,如 (void *)1。 也可以传 malloc 分配的堆内存地址。 【大坑】绝不能传线程内部栈区局部变量的地址! 因为线程退出后栈区会被释放,主线程 join 拿到的是野指针。 返回值:无(该函数调用后线程即终止,不会返回到调用者)。 对比:exit() 是终止整个进程(所有线程),return 只能在主函数或线程函数里用。 如果在子线程中调用 exit(),整个程序都会挂掉!

2. 请求取消线程

int pthread_cancel(pthread_t thread); pthread_t thread 入参:要取消的目标线程的 ID。 返回值:成功返回 0;失败返回错误码(非0)。 【重要机制】:pthread_cancel 只是“提出请求”,并不是立刻“强杀”。 目标线程必须运行到“取消点”(Cancellation Point,如 sleep, read, write 等系统调用,或主动调用 pthread_testcancel)才会真正退出。 如果目标线程是一个没有取消点的死循环(比如 while(1){}),它是不会理睬这个取消请求的。

3. 阻塞回收

int pthread_join(pthread_t thread, void **retval); pthread_t thread 入参:要回收(等待)的目标子线程 ID。 void **retval 出参:二级指针,用于接收子线程退出的返回值(即 pthread_exit 或 return 传出的东西)。 如果只传 NULL,表示只回收资源,不关心返回值。 【用法示例】: void *ret = NULL; pthread_join(tid, &ret); // 此时 ret 就拿到了子线程的遗言。 返回值:成功返回 0;失败返回错误码(非0)。 【注意】: 1. 这是阻塞函数,调用它的线程(通常是主线程)会一直卡在这里,直到目标线程结束。 2. 一个线程只能被 join 一次。 3. 线程不能 join 自己。 4. 已经被 pthread_detach 分离的线程,不能再 join。

4. 设置分离属性

int pthread_detach(pthread_t thread); pthread_t thread 入参:要设置为分离状态的目标线程 ID。 如果在子线程内部想分离自己,填 pthread_self() 即可。 返回值:成功返回 0;失败返回错误码(非0)。 【作用】:告诉系统:“这个线程死掉后,它的资源(如 8MB 的栈区)你直接帮我回收,不用等别人 join”。 【注意】: 1. 分离属性一旦设置,就无法再改回可结合(joinable)状态。 2. 如果设置分离,主线程就不能再调用 pthread_join,否则会报错(EINVAL)。 3. 常用来处理“不需要关心运行结果、跑完拉倒”的后台任务。

四、线程控制

1. 互斥

概念:在多线程中对临界资源的排他性访问。

临界资源:多线程中会被读写操作的资源(全局变量、文件、设备)。

排他访问:同一时刻只能有一个线程进行读写操作。

为什么需要:多线程并发穿插执行时,像 A++ 这种操作在汇编层面至少是 3 句指令。如果线程 1 执行一半被切走,线程 2 接着执行,会导致数据一致性问题(竞态条件)。

使用步骤:定义锁 -> 初始化锁 -> 加锁 -> 解锁 -> 销毁。

1.1 互斥相关函数
1. 定义互斥锁 pthread_mutex_t mutex; 2. 初始化锁 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); 功能:将已经定义好的互斥锁初始化。 参数:mutex 要初始化的互斥锁。 attr 初始化的值,一般是 NULL 表示默认锁。 返回值:成功 0;失败 非零。 3. 加锁 int pthread_mutex_lock(pthread_mutex_t *mutex); 功能:用指定的互斥锁开始加锁代码。 加锁后的代码到解锁部分属于【原子操作】,在加锁期间其他线程不能操作该部分代码。 如果 mutex 已经被其他线程使用,则该函数会【阻塞】。 参数:mutex 用来给代码加锁的互斥锁。 返回值:成功 0;失败 非零。 4. 解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex); 功能:将指定的互斥锁解锁。 解锁之后代码不再排他访问,一般加锁解锁成对出现。 参数:mutex 用来解锁的互斥锁。 返回值:成功 0;失败 非零。 5. 销毁锁 int pthread_mutex_destroy(pthread_mutex_t *mutex); 功能:使用互斥锁完毕后需要销毁互斥锁。 参数:mutex 要销毁的互斥锁。 返回值:成功 0;失败 非零。

2. 同步

概念:有一定先后顺序的对资源的排他性访问。
用途:在多线程中,让线程按照一定顺序运行。、

使用步骤:定义信号量 -> 初始化 -> PV 操作 -> 销毁。

2.1 同步与互斥的区别:

互斥包含同步,同步是互斥的特例。
操作线程:互斥锁的加锁和解锁必须是同一个线程;信号量(同步)是由线程交叉释放(th1 释放 th2,th2 释放 th1)。
临界区要求:互斥锁保护的代码(临界区)要求短小精悍,不能有休眠或大耗时操作;信号量适当可以有休眠或小的耗时操作。
计数信号量:信号量的初值可以大于 1(如 3、5),用于资源本身不唯一的互斥情况(多个相同资源)。

2.2 同步相关函数
1. 定义信号量 sem_t sem; 2. 初始化信号量 int sem_init(sem_t *sem, int pshared, unsigned int value); 功能:将已经定义好的信号量赋值。 参数:sem 要初始化的信号量。 pshared =0 表示线程间使用信号量;!=0 表示进程间使用。 value 信号量的初始值。一般无名信号量都是二值信号量(0 或 1)。 0 表示红灯(阻塞);1 表示绿灯(通行)。 返回值:成功 0;失败 -1。 3. P 操作(申请资源) int sem_wait(sem_t *sem); 功能:判断当前 sem 是否有资源可用。 有资源(==1)则申请资源,程序继续运行; 没有资源(==0)则线程【阻塞等待】,一旦有资源则自动申请并继续运行。 注意:申请资源后会自动执行 sem = sem - 1。 参数:sem 要判断的信号量资源。 返回值:成功 0;失败 -1。 4. V 操作(释放资源) int sem_post(sem_t *sem); 功能:将指定的 sem 信号量资源释放。 默认执行 sem = sem + 1;线程在该函数上【不会阻塞】。 参数:sem 要释放资源的信号量。 返回值:成功 0;失败 -1。 5. 销毁信号量 int sem_destroy(sem_t *sem); 功能:使用完毕将指定的信号量销毁。 参数:sem 要销毁的信号量。 返回值:成功 0;失败 -1。

3. 死锁(Deadlock)

概念:由于锁资源安排得不合理(锁资源的申请和释放逻辑不对),导致进程或线程无法正常继续执行(推进)的现象。

产生死锁的四个必要条件(必须同时满足才会发生死锁):

互斥条件:一个资源每次只能被一个进程使用。

请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。

循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

五、注意事项

1. 资源与内存

1.1 栈独立,堆共享:独立栈区存局部变量。堆、全局/静态变量、文件描述符由所有线程共享,多线程读写共享数据必须加锁(如 pthread_mutex_t)。
1.2 传参生命周期:arg 为 void*。避免传局部变量地址,防止主线程提前退出或变量失效;推荐:malloc 结构体传参,或将值直接强转指针 (void*)100。
1.3 返回值生命周期:pthread_exit 和 return 的返回指针,禁止指向线程局部变量(栈区销毁会变野指针);推荐:malloc 堆内存返回,主线程 join 后 free;或直接强转值返回 (void*)1。
1.4 禁用 exit:子线程调用 exit() 会终止整个进程。子线程退出只能用 pthread_exit 或 return。

2. 核心 API 细节

2.1 pthread_create 返回值:成功返回 0,失败返回错误码(非 -1),不修改 errno。需用 strerror(ret),不能用 perror。
2.2 pthread_self 打印:Linux 下 pthread_t 为无符号长整型,打印需强转:printf("%lu", (unsigned long)pthread_self());
2.3 pthread_cancel 取消点:发送取消请求,目标线程必须运行至“取消点”(如 sleep, read, pthread_testcancel)才会退出。无取消点的纯死循环无法被取消。
2.4 pthread_join 约束:阻塞等待;只能 join 一次;不能自 join;不能 join 已 detach 的线程。

3. 生命周期与回收

3.1 默认属性(joinable):线程退出后栈区和控制块不自动释放。不 join 也不 detach 会导致僵尸线程(内存泄漏)。
3.2 pthread_detach:设置分离后,线程退出时系统自动回收资源;不可逆:分离后绝不能再次 join。
3.3 主线程退出:主线程 return 或 exit() 会强杀所有子线程。需等待子线程时,必须用 pthread_join 或 pthread_exit(NULL)。

4. 编译与调试

4.1 编译选项:必须加 -pthread:gcc test.c -o test -pthread
4.2 ps 查看线程
ps -eLf:看 NLWP(线程数)、LWP(线程ID)。
ps -eLo pid,ppid,lwp,stat,comm:自定义状态。主线程 LWP = PID;子线程 LWP 独立;STAT 状态:S(睡眠), R(运行), D(不可中断IO), Z(僵尸)。

5. 核心口诀

传参防野指针,返回必用 malloc;
退出千万别用 exit,死循环里加取消点;
建了线程必须收尸(join/detach),编译别忘 -pthread。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 2:42:08

【皓月当空】黄金多空对冲·超级刷单版

本EA采用多空双向挂单对冲思路&#xff0c;自带加仓网格机制、总亏损强平、手数上限与跟踪止损功能&#xff0c;适合震荡行情下自动化刷单交易。 策略特点&#xff1a; ✔ 多空双向挂单&#xff0c;震荡行情双向捕捉机会&#xff1b; ✔ 网格加仓机制&#xff0c;固定间距加仓&…

作者头像 李华
网站建设 2026/10/30 2:40:22

抖音视频如何转音频?IOS录音软件能帮忙吗

手机录屏截取视频音频&#xff0c;是很多人处理短视频内容时的第一步。但实际操作中有几个明显问题&#xff1a;系统提示音混入背景声、横屏录屏后音频轨道位置偏移、后台切换导致片段丢失。iOS系统对后台录屏的管控较严&#xff0c;连续录制超过10分钟后出现降频或中断的概率不…

作者头像 李华
网站建设 2026/10/30 2:40:10

Day01_Claude Code 实战指南:启动方式、权限配置、常用命令与项目宪法

一、Claude Code 的启动方式场景命令说明常规启动claude进入交互式会话&#xff0c;开启一个全新的对话上下文继续上一次对话claude --continue 或 claude -c直接接着上一次的会话继续&#xff0c;适合中断后恢复工作恢复历史聊天记录claude --resume 或 claude -r列出历史会话…

作者头像 李华
网站建设 2026/10/30 2:34:34

鸿蒙多窗适配:不要把窗口宽度当成内容区宽度【鸿蒙心迹】

TL;DR:多窗与分栏下应以实际内容区而非窗口总宽度驱动断点。本文用 ContainerReader 拆解测量对象、布局决策与回归检查;示例仅作接口说明,尚未完成本机 SDK 或真机编译运行。 多窗把页面挤坏了:不要把窗口宽度当成内容区宽度【鸿蒙心迹】 > 适用场景:同一页面会出现在…

作者头像 李华