本周改用引导提问式学习
注释解释
【重点】必须能理解、能写进代码、也能向老师说清楚。
【易错点】最容易写错或说错的地方,运行前要检查。
【专业表达】把生活类比换成嵌入式 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
读代码时只抓住这条主线
-
程序先打印原进程 PID。
-
fork 创建子进程。
-
子进程进入 pid == 0 分支,完成三个订单。
-
父进程进入 pid > 0 分支,用 wait 等待。
-
子进程结束后,父进程打印 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。先看提示符,避免把命令输错终端。
七 老师提问时的专业描述模板
不要只说"开了两家店"和"多了两个员工"。类比用于理解,汇报时再换成下面的专业说法。
-
我使用 fork 创建子进程,并根据 fork 的返回值区分父进程和子进程。
-
fork 在子进程中返回 0,在父进程中返回子进程 PID,失败时返回负值。
-
父进程通过 wait(NULL) 等待子进程结束,保证最后的完成信息在子进程结束后输出。
-
我使用 pthread_create 创建厨师和打包两个工作线程,并使用 pthread_join 等待线程结束。
-
父子进程或多个线程的执行顺序由操作系统调度,因此输出顺序不固定。
-
程序先在 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。
十 合上文档后的自测
-
用生活类比和专业术语分别解释进程与线程。
-
说出 fork 的三种返回情况。
-
说明 fork 返回值和 getpid() 有什么区别。
-
说明 wait 和 pthread_join 分别等待什么。
-
解释为什么 Ubuntu 与开发板的线程输出顺序不同。
-
完整描述从 Ubuntu 源代码到开发板运行的流程。