第三周学习文档 Linux C 多进程与多线程

本周改用引导提问式学习

注释解释

【重点】必须能理解、能写进代码、也能向老师说清楚。

【易错点】最容易写错或说错的地方,运行前要检查。

【专业表达】把生活类比换成嵌入式 Linux 的标准术语。

一 先用外卖店建立整体认识

进程像两家独立的店

原来只有一家外卖店。调用 fork 后,像复制出第二家店。两家店都可以继续接单,但通常各自使用自己的空间和数据。

父进程 原来的外卖店。

子进程 fork 创建出来的新店。

【专业表达】进程是正在运行的程序实例。父进程调用 fork 创建子进程,父子进程拥有相互独立的地址空间。

线程像同一家店里的多名员工

店没有增加,但店里多了厨师和打包员。他们在同一个店内完成不同工作,并共享订单、库存等资源。

主线程 店长,负责创建员工并等待工作结束。

工作线程 厨师或打包员,执行指定的线程函数。

【专业表达】线程是进程中的执行单元。同一进程内的多个线程共享该进程的地址空间和资源。

【重点】进程强调相互独立,线程强调同一进程内共享资源。

你应该能够这样回答老师

好懂说法:进程像两家店,线程像一家店里的多个员工。

专业说法:进程是资源分配的基本单位,线程是进程中的执行单元;同一进程内的线程共享进程资源。

二 fork 多进程核心知识

fork 创建一个子进程。调用后,父进程和子进程都会从 fork 后面的代码继续运行。

pid_t Linux 用来表示进程编号或 fork 返回值的类型。

getpid 取得当前进程自己的真实 PID。

wait 让父进程等待子进程结束。

【重点】fork 只调用一次,却会有父进程和子进程两条执行路线。

fork 返回值是判断身份的依据

复制代码
pid_t pid;
pid = fork();

if (pid < 0)
/* create failed */
else if (pid == 0)
/* child process */
else
/* parent process *

pid < 0 创建失败。

pid == 0 当前代码运行在子进程中。

pid > 0 当前代码运行在父进程中,这个值是子进程的 PID。

【易错点】子进程中的 pid 等于 0,不代表子进程的真实 PID 是 0。子进程自己的 PID 要用 getpid() 获取。

【专业表达】fork 在父进程中返回子进程 PID,在子进程中返回 0,失败时返回负值。

为什么需要 wait

好懂说法:原店先别关门,要等新店完成工作。

【专业表达】父进程调用 wait(NULL) 后会等待子进程结束,并回收子进程的退出状态。

【易错点】wait 不是创建进程的函数;创建进程使用 fork,wait 只负责等待。

执行顺序为什么不固定

父进程和子进程都由 Linux 调度。谁先获得 CPU,谁就可能先打印,所以每次输出顺序可能不同。

【专业表达】父子进程并发执行,具体先后顺序由操作系统调度决定,具有不确定性。

【重点】wait 只能保证父进程在等待位置之后的代码晚于子进程结束,不能保证 wait 之前所有输出的顺序。

三 fork 完整案例

下面的程序创建一个子进程。子进程配送三个订单,父进程等待配送完成后再结束。

复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
pid_t pid;
int i;

printf("Original shop starts. PID=%d\n", getpid());
pid = fork();

if (pid < 0)
{
printf("Fork failed.\n");
return 1;
}
else if (pid == 0)
{
for (i = 1; i <= 3; i++)
{
printf("Child shop delivers order %d\n", i);
}
}
else
{
wait(NULL);
printf("All orders finished\n");
}

return 0;
}

编译和运行

gcc fork_demo.c -o fork_demo

./fork_demo

读代码时只抓住这条主线

  1. 程序先打印原进程 PID。

  2. fork 创建子进程。

  3. 子进程进入 pid == 0 分支,完成三个订单。

  4. 父进程进入 pid > 0 分支,用 wait 等待。

  5. 子进程结束后,父进程打印 All orders finished。

【易错点】不要把父子进程的输出理解成程序被手动运行了两次。一次运行中,fork 创建了第二个进程。

你的实际运行结果说明了什么

Original shop starts. PID=10171

Child shop delivers order 1

Child shop delivers order 2

Child shop delivers order 3

All orders finished

这说明子进程成功创建,并且父进程确实等到子进程完成后才输出最后一行。

四 pthread 多线程核心知识

pthread_t保存线程标识。

pthread_create 创建一个新线程,并指定它执行哪个函数。

线程函数 新线程真正执行的代码。

pthread_join 让主线程等待指定工作线程结束。

【重点】一个进程原本就有主线程。再创建厨师和打包线程后,总共是一个进程、三个线程。

线程函数的标准形式

void *worker(void *arg)
{
/* thread work */
return NULL;
}

目前不需要深入研究 void 指针。先会识别:pthread_create 要求线程入口函数使用这种形式。

【专业表达】pthread_create 创建线程,并把线程入口函数作为参数传入;新线程从该函数开始执行。

pthread_create 四个参数怎样看

pthread_create(&cook_thread, NULL, cook_food, NULL);

第一个参数 保存新线程标识。

第二个参数 线程属性,NULL 表示使用默认属性。

第三个参数 线程要执行的函数。

第四个参数 传给线程函数的参数,本例暂时不用,所以写 NULL。

【易错点】pthread_create 的第三个参数写函数名,不要写成 cook_food()。写括号表示立即调用函数,不是把函数交给新线程。

pthread_join 和 wait 的区别

wait 等待子进程,pthread_join 等待指定线程。它们都能让当前执行者先等别人完成,但等待对象不同。

【专业表达】主线程通过 pthread_join 等待工作线程退出,保证主进程不会在工作线程完成前结束。

五 pthread 完整案例

下面在同一个进程中创建厨师和打包两个线程。两个线程分别循环三次,主线程等待它们全部结束。

cs 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void *cook_food(void *arg)
{
int i;

for (i = 1; i <= 3; i++)
{
printf("Cook prepares order %d.\n", i);
usleep(200000);
}

return NULL;
}

void *pack_food(void *arg)
{
int i;

for (i = 1; i <= 3; i++)
{
printf("Packer packs order %d.\n", i);
usleep(200000);
}

return NULL;
}

int main(void)
{
pthread_t cook_thread;
pthread_t pack_thread;

pthread_create(&cook_thread, NULL, cook_food, NULL);
pthread_create(&pack_thread, NULL, pack_food, NULL);

pthread_join(cook_thread, NULL);
pthread_join(pack_thread, NULL);

printf("All work is finished.\n");
return 0;
}

编译和运行

gcc thread_demo.c -o thread_demo -pthread

./thread_demo

【重点】-pthread 告诉编译器和链接器使用 POSIX 线程支持。

【易错点】忘记 -pthread 可能导致编译或链接失败。

为什么输出顺序会变化

Ubuntu 中先看到 Cook,开发板上却先看到 Packer。这不是错误,而是两个线程被调度的先后顺序不同。

【专业表达】多个线程并发执行,输出顺序具有不确定性;pthread_join 只保证主线程等待它们全部结束。

六 交叉编译和开发板验证

Ubuntu 的 gcc 生成 x86 程序,不能直接给 ARM 开发板运行。需要使用 ARM 交叉编译器生成开发板可执行文件。

cs 复制代码
arm-linux-gnueabihf-gcc -static fork_demo.c -o fork_demo_arm
arm-linux-gnueabihf-gcc -static thread_demo.c -o thread_demo_arm -pthread
file fork_demo_arm
file thread_demo_arm

【专业表达】使用 arm-linux-gnueabihf-gcc 将源程序交叉编译为 ARM 32 位静态链接可执行文件。

【易错点】不要把普通 gcc 生成的 x86 可执行文件拿到 i.MX6ULL 上运行。先用 file 检查是否显示 ARM。

开发板运行

cs 复制代码
/mnt/nfs/fork_demo_arm
/mnt/nfs/thread_demo_arm

本次两个 ARM 程序均已在开发板运行成功。线程程序在开发板上先输出 Packer,再输出 Cook,进一步证明线程顺序不固定。

重启后 NFS 目录为空怎么办

你已经实际遇到过:开发板重启后地址丢失,/mnt/nfs 为空。先恢复双方地址,再重新挂载 NFS。

cs 复制代码
# Ubuntu terminal
sudo ip addr add 192.168.10.10/24 dev ens33

# Board terminal
ifconfig eth1 192.168.10.20 netmask 255.255.255.0 up
ping -c 4 192.168.10.10
mount -t nfs -o nolock,vers=3 192.168.10.10:/home/zyy/nfs_share /mnt/nfs

【易错点】Ubuntu 使用 ens33,开发板使用 eth1。先看提示符,避免把命令输错终端。

七 老师提问时的专业描述模板

不要只说"开了两家店"和"多了两个员工"。类比用于理解,汇报时再换成下面的专业说法。

  1. 我使用 fork 创建子进程,并根据 fork 的返回值区分父进程和子进程。

  2. fork 在子进程中返回 0,在父进程中返回子进程 PID,失败时返回负值。

  3. 父进程通过 wait(NULL) 等待子进程结束,保证最后的完成信息在子进程结束后输出。

  4. 我使用 pthread_create 创建厨师和打包两个工作线程,并使用 pthread_join 等待线程结束。

  5. 父子进程或多个线程的执行顺序由操作系统调度,因此输出顺序不固定。

  6. 程序先在 Ubuntu 上验证逻辑,再使用 ARM 交叉编译器生成可在 i.MX6ULL 上运行的文件,并通过 NFS 完成开发板验证。

八 本周重点速记

【重点】fork 创建进程,pthread_create 创建线程。

【重点】wait 等子进程,pthread_join 等线程。

【重点】getpid() 得到当前进程自己的 PID。

【重点】进程相互独立,线程共享同一进程的资源。

【重点】并发程序的输出顺序通常不固定。

【重点】线程程序编译要加 -pthread,开发板程序要使用 ARM 交叉编译器。

九 本周易错点总检查

把 fork 返回 0 误认为创建失败。

把 fork 返回值和 getpid() 的结果混为一谈。

忘记 wait,或把 wait 误认为创建进程。

pthread_create 第三个参数错误地写成带括号的函数调用。

忘记 pthread_join 或编译时忘记 -pthread。

认为 Cook 和 Packer 必须固定顺序输出。

用普通 gcc 为开发板生成程序,或混淆 Ubuntu 的 ens33 和开发板的 eth1。

十 合上文档后的自测

  1. 用生活类比和专业术语分别解释进程与线程。

  2. 说出 fork 的三种返回情况。

  3. 说明 fork 返回值和 getpid() 有什么区别。

  4. 说明 wait 和 pthread_join 分别等待什么。

  5. 解释为什么 Ubuntu 与开发板的线程输出顺序不同。

  6. 完整描述从 Ubuntu 源代码到开发板运行的流程。

相关推荐
奕鼎竜瑆2 小时前
无监督学习:聚类与降维
学习·数据挖掘·聚类
此生决int2 小时前
Linux系列(06)——进程概念(上)
linux·运维·服务器
听风若依3 小时前
函数栈帧的创建与销毁-重置版
c语言·学习
Wang's Blog3 小时前
Java 项目部署之 Docker工具快速入门: 学习路径与命令速查导引
java·学习·docker
程序猿阿森3 小时前
Warmup 学习率预热详解:为什么训练初期要“先热身再加速”?
深度学习·学习·语言模型
H.莓飛3 小时前
【C++】内存管理
linux·开发语言·jvm·c++·visual studio
励志师code3 小时前
RoCE 网络实战:从 NCCL 调优到 RDMA 硬件计数器排障
linux·运维·网络·云计算
水饺编程3 小时前
第2章:Linux 基础,pwd 命令
linux·c语言·汇编
lisw053 小时前
人类认知和语言学习中的控制论!
人工智能·学习·数学建模