自学嵌入式第N+1天(Linux——多任务编程)
多任务编程也叫进程编程,进程就是程序的一次执行过程,或者说是正在运行中的程序。
我们在用电脑时可以一边打游戏一边听歌,还可以回消息等等,这些看起来是同时在运行的,但其实是每个程序只跑一小会儿就被踢下去,换下一个。因为切换太快(毫秒级),人眼 / 人感官根本察觉不到,就以为它们在同时跑。这叫做宏观上并行,微观上串行。
那么是怎么做到让每个程序都跑一小会呢?这里就要提到调度算法了,比如时间片轮转,操作系统给每个进程分配一个时间片(time slice),通常是 几毫秒到几十毫秒(比如 Linux 默认约 100ms 量级,实际更复杂),然后每个程序依次运行,然后循环往复(A->B->C->D->A->B.....)。
一、基础知识
1.程序与进程,线程
| 概念 | 本质 | 通俗解释 |
|---|---|---|
| 程序 | 存放在磁盘上的可执行文件(静态) | 菜谱,放在书架上,不会自己动 |
| 进程 | 程序的一次执行过程(动态),有独立的地址空间 | 按照菜谱做菜的一个厨房,有自己的锅碗瓢盆(内存) |
| 线程 | 进程内部的一条执行流,共享进程的地址空间 | 同一个厨房里的多个厨师,共用锅碗瓢盆,但各做各的菜 |
程序=数据+代码=data(已初始化的数据)+bss(未初始化的数据)+text(正文段,也就是代码区)
进程=PCB+[栈|堆|data|bss|text]
PCB块:是用来记录和保存程序运行过程中的动态信息,方便操作系统调度。
线程下一篇再讲
2.进程的一生
(1)进程的通用模型

所有OS的进程都符合这个模型,新建状态表示进程正在被创建,还没有准备好;就绪状态代表进程一切已经就绪,等待CPU调度运行;运行状态是进程正在CPU上执行;阻塞状态指进程在等待IO、资源等某个事件,当等待的事件发生后,进程会回到就绪状态;终止状态则是进程执行完毕,正在被撤销。
(2)linux特有

| 状态 | 含义 |
|---|---|
| 就绪 | Linux 不区分就绪和运行,都叫 RUNNING。区别只在于 "是否正占有 CPU"。图里分成了 "就绪" 和 "占有 CPU 执行" 两个圈,但内核里是同一个状态。 |
| 浅度睡眠 | 等资源,可以被信号唤醒(比如 sleep、等键盘输入)。你 Ctrl+C 能打断它就是因为这个。 |
| 深度睡眠 | 等硬件 IO(比如读磁盘),不能被信号打断,只能等资源到位 wake_up() 唤醒。这就是为什么有时候进程卡死 kill -9 都杀不掉 —— 它在这个状态。 |
| 暂停 | 收到 SIGSTOP/SIGTSTP 信号被挂起,或者调试时 ptrace 暂停。收到 SIGCONT 恢复。 |
| 僵死 | 进程已经执行完 do_exit(),但父进程还没 wait() 回收它,PCB 还留着。 |
3.状态编码
R = RUNNING:就绪和正在运行
S = INTERRUPTIBLE:浅睡,最常见
D = UNINTERRUPTIBLE:深睡,杀不死的那种
T = STOPPED:被任务控制信号暂停
t:debug暂停
Z = ZOMBIE:僵尸态,被接管
X=dead:资源被回收了,永远看不到
4.常用命令
下面将展示在Vmware中常用的一些命令以及他的运行效果
(1)ps aux | grep a.out

| 列 | 含义 |
|---|---|
| USER | 进程所属用户,这里都是linux普通用户 |
| PID | 进程 ID 号,每个进程唯一编号(kill 杀进程就用这个数字) |
| %CPU | CPU 占用百分比,你 7256 达到 89.3%,CPU 疯狂跑死循环 |
| %MEM | 占物理内存百分比 |
| VSZ | 虚拟内存大小,单位 KB |
| RSS | 实际占用物理内存,单位 KB |
| TTY | 终端。pts/0你运行程序的终端窗口;pts/1是你敲 ps 命令的另一个终端 |
| STAT | 进程状态,最重要一列 |
| START | 进程几点启动,19:32 |
| TIME | 进程累计占用 CPU 时间 |
| COMMAND | 启动这个进程的命令 |
(2)ps -eLf | grep a.out

| 字段 | 含义 |
|---|---|
| UID | 运行进程的用户 ID,这里都是linux |
| PID | 进程 ID 号 |
| PPID | 父进程 PID(重点!看谁 fork 出来的) |
| LWP | 轻量级线程 ID,单进程单线程时 LWP=PID |
| C | CPU 占用调度计数 |
| NLWP | 该进程一共有多少个线程数 |
| STIME | 进程启动时间 |
| TTY | 属于哪个终端,pts/0终端 0 |
| TIME | 累计占用 CPU 时间 |
| CMD | 执行的命令 |
(3)看进程树
pstree可以看到整个进程树。
pstree -sp pid号:可以看到从1到当前进程的

(4)kill发一个信号给进程
用法:kill -信息编号 进程pid号
信息编号有:
9:SIGKILL让进程结束杀死进程
19:暂停进程
18:恢复进程
killall -9 a.out 可以杀死所有进程。
二、代码实现
1.创建一个子进程
形式:pid_t fork(void)
头文件:#include<sys/types.h> #include<unistd.h>
功能:复制调用fork的进程,来创建一个新进程
返回值:成功时父进程返回子进程的pid号,子进程返回0,失败时返回-1,&errno
注意:在创建完进程后父进程和子进程谁先运是不确定的,和进程的调度算法,优先级等因素有关,也不一定是一个进程执行一次,运行到哪取决于操作系统怎么调度。
getpid()能返回当前进程的pid号。
要注意在代码中什么位置的的代码会被什么执行。

例题:
(1)连续fork两次

第一次父进程(7910)创建了一个子进程(7911),第二次fork时父进程又创建(7912),因为子进程会复制父进程,所以子进程也会执行一次fork创建(7913)。
(2)fork()&&fork()||fork()


这里面涉及逻辑运算的短路特性,以及前面提到的子进程的返回值为0,父进程的返回值是子进程的pid号(一定>0),例如在(1)生成的子进程(7944)中fork()的返回值为0那么fork()&&fork()就直接不计算了,直接计算(3),以及fork()&&fork()为1时,整体一定为真(3)也不会继续进行。
注意:fork之后字进程继承了什么?
| 继承了 | 没继承 |
|---|---|
| 代码段、数据段、堆、栈(写时复制) | PID |
| 文件描述符(打开的文件) | 父进程的 PID |
| 环境变量、当前工作目录 | 未处理的信号 |
| 用户 ID、组 ID | 锁(有些锁不继承) |
要注意父子进程拥有独立的4G内存空间,各自拥有独立的数据段,数据之间不会相互影响,无法进行数据传递。但是这种又好又坏,好处是如果子进程崩了,其他进程不会受到影响,只是交换数据不太方便。
(3)文件
复制一个文件,父进程复制前半部分,子进程复制后半部分。


fork之后,父子进程文件共享:子进程会继承父进程中已经打开的文件描述符 ,父子进程使用的是同一个文件表,相互之间的操作会受到影响,如果不想相互间受到影响可以fork之后再打开文件,父子进程各自独立操作 。
因为我们不能确定是先运行父进程还是先运行子进程,所以在父进程复制前半部分的时候一定要将读写指针移动到开头。
2.进程执行
exec 系统调用能够加载磁盘上其他独立的可执行程序,使进程可以运行不属于当前程序的代码;普通函数调用只能执行本程序内部已经编译好的代码。exec 实现程序之间的解耦。就像先租一间新房子(fork 得到新 PID),然后把新房子里面全部东西清空(替换进程镜像),叫外面完全另一队工人(外部可执行程序)搬进来干活。
(1)int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
参数:@path ---表示要执行的文件的 路径 --- 要求路径名中 要包含可执行文件名
@arg ---表示的是可执行文件的名字
@... ---可变参数 注意: 最终以NULL作为结尾,这块往往给的是,可执行文件需要用到的一些参数
区别:list --- 逐个罗列,从第二个参数开始,逐个罗列
vector --- 表示 从第二个参数开始 组织成 数组的形式
which 文件名:可以查看可执行文件的文件路径。
用法:他们可以达到相同的效果,区别就是execv需要把参数写在一个数组里面。

(2)int execlp ( const char *file, const char *arg, ...);
int execvp ( const char *file, char *const argv[]);
带P(PATH)表示寻找可执行文件,到系统的环境变量PATH中寻找,此时不需要指定路径
@file参数 --只需要指定可执行文件的名字即可
其余参数与上一个相同,这里就不举例了。
(3)int execle(const char *path, const char *arg,..., char * const envp[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
如果要执行的文件,需要使用到环境变量,则可以通过带e的这两个函数传递环境变量个可执行文件。主播不太懂这个环境变量有什么用啊,先记住这个函数吧,如果后面学明白了会在之后的博客仔细讲的。
上面这六个函数经常和下面这个函数结合使用
char *strtok(char *str, const char *delim);
功能:提取字符串
参数:@str为要解析字符串,给成NULL 表示接着原先的字符串继续拆分
@delim 为分隔标记,遇到这些符号就会自动将前面的作为一个字符串。
返回值:成功 返回子字符串的首地址,失败 返回NULL,字符串解析完,也返回NULL
例子:当遇到' ''\n'时拆分,第二句为连续拆分的使用方法。
![]()
3.进程退出
(1)进程结束:
A.正常结束:main函数中 return;exit() 库函数;_exit,_Exit系统调用;主线程退出;主线程调用pthread_exit
B.异常结束(往往是因为信号导致的结束):abort() 发送一个SIGABRT;signal 发信号结束了进程 kill pid -9 ;最后一个线程被pthread_cancle
(2)函数
exit 和_exit退出状态,终止的进程会通知父进程,自己使如何终止的。如果是正常结束(终止),则由exit传入的参数带回。如果是异常终止,则有内核通知异常终止原因的状态。任何情况下,父进程都能使用wait,waitpid获得这个状态,以及资源的回收。
a.void exit(int status)
功能:让进程退出,并刷新缓存区
参数:status:进程退出的状态
exit(int status)参数是完整 int(32 位),Linux 内核只保存 status 的低 8 位给父进程,有效范围:0~255,所以需要将后8位提取出来所以status&0377可以得到真正有效的退出码。
EXIT_SUCCESS 0
EXIT_FAILURE 1
我们可以使用这两个宏定义,让代码更直观,0代表正常终止,1代表失败。
b._exit
void _exit(int status);
功能:让进程退出,不刷新缓存区
参数:status:进程退出状态
exit和_exit的区别

exit -> 刷新缓存区 -> atexit注册的退出函数 -> _exit
c.int atexit(void (*function)(void));
功能:注册进程退出前执行的函数
参数:function:函数指针,指向void返回值void参数的函数指针
返回值:成功返回0,失败返回非0
作用:关闭你自己打开的文件;释放堆上 malloc 的内存;写日志,记录程序正常退出;清理临时文件。
当程序调用exit或者由main函数执行return时,所有用atexit注册的退出函数,将会由注册时顺序倒序被调用(可以多次注册)。
4.资源回收
a.pid_t wait(int *wstatus);
头文件:#include <sys/types.h> #include <sys/wait.h>
功能:该函数可以阻塞等待任意子进程退出,并回收该进程的资源和获取退出状态值。一般用于父进程回收子进程状态。
参数:status为进程退出时候的状态,如果不关心其退出状态一般用NULL表示,如果要回收进程退出状态,则用WEXITSTATUS回收。
返回值:成功 回收的子进程pid,失败 -1

WIFEXITED(wstatus) //判断是否是正常结束
WEXITSTATUS(wstatus) //获取正常结束时的退出状态值
WIFSIGNALED(wstatus) //判断是否时信号结束的
WTERMSIG(wstatus) //是哪个信号
b.pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收子进程状态
参数:pid为子进程的pid号,wstatus有以下四种选择:
<-1 回收指定进程组内的任意子进程
-1 回收任意子进程,组内外
0 回收和当前调用waitpid一个组的所有子进程,组内
>0 回收指定ID的子进程
options为0时是阻塞等待,为WNOHANG表示非阻塞等待。阻塞等待是父进程会停留在这一句等待子进程退出。
返回值:成功 返回接收资源的子进程pid,非阻塞是返回值为 0 表示没有任何子进程状态改变,失败 -1 。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)