嵌入式Linux C多任务编程(进程篇)

  • Post author:
  • Post category:linux


这俩天刚整理完进程部分内容,再做个一个总结以便后期回顾。

1.什么是多任务?

单任务vs多任务


单任务

:一个任务执行结束才能执行下一个任务,或者说在一个任务执行得过程中不能响应其他任务,只有这个任务执行完毕后才能去响应其他任务,这种成为单任务。


多任务

:cpu利用它的资源响应多个任务,注意只是响应,但不一定时同时响应,而且也并不是同时执行。

并发vs并行


并发

:一对多


并行

:多对多

单核cpuvs多核cpu


单核cpu

:就是CPU轮换的执行,当前进程执行了一个短暂的时间片(ms)后,切换执行另一个进程,如此循环往复,由于时间片很短,在宏观上我们会感觉到所有的进程都是在同时运行的,但是在微观上cpu每次只执行某一个进程的指令。


多核cpu

:如果cpu是多核的话,不同的cpu核可以同时独立的执行不同的进程,这种叫并行运行。所以当cpu是多核时,并发与并行是同时存在的。

单核系统的同时执行是并发执行,多核系统的同时执行是并行执行。同时执行必须建立在多核的多任务操作系统的基础上才能完成。

2.多任务操作的实现方式

进程、线程

3.进程和程序的区别

进程:

①进程是程序运行的抽象。(抽象是为了方便系统的统一管理)

②进程是系统资源分配的最小单位。

③进程和线程实现多任务(并发执行)。

区别:

①程序是静态的,进程是动态的。

②程序是长久的,进程是暂时的。

③进程与程序组成不同,进程的组成包括程序、数据和进程控制块。

④进程与程序的对应关系:通过多次执行,一个程序可以对应多个进程;通过调用关系,一个进程可以包括多个程序。

4.Linux进程特点

Linux系统是一个多进程的系统,它的进程之间具有

并行性



互不干扰

等特点。

每个进程都是一个独立的运行单位,拥有各自的权力和责任。每个进程都运行在独立的虚拟地址空间,当一个进程发生异常,它也不会影响到系统中其他进程。


优点

:进程的互不干扰和独立地址空间使进程成为一种

安全的多任务机制


缺点



开销大

(进程的创建、进程之间的切换消耗的内存资源多)

5.进程PID

为了方便管理,为每一个进程分配了一个唯一的PID编号。

OS(Linux)为了管理进程,会为每一个进程创建一个task_struct结构体变量,里面存放了该进程的管理信息。


获取PID



父进程创建子进程。



pid_t getpid(void);

函数作用:获取调用该函数进程的进程PID。

头文件:


#include <unistd.h>


#include <sys/types.h>

返回值:返回进程的PID。

②pid_t getppid(void);

函数作用:获取调用该函数进程的父进程的进程PID。

返回值:返回父进程的PID

6.ps命令查看进程

查看进程的方式有三种:

①ps -ax :

ps -ax | grep main



top



htop

top/htop都是显示整个系统下的进程的详细信息,htop是第三方的,是对top的改进。

下面是关于ps命令后缀的详注:

-A/-e: 显示系统所有的进程(包括守护进程),相当于-e。

-a:显示所有终端下的所有用户运行的进程。

-u:显示所有用户名、CPU百分比和内存的使用。

-x/-f:列出进程的详细信息。

-H:显示进程树。

-r:只显示正在运行的进程。

-o:分类输出

7.进程调度


a)进程状态


1)基本三态:就绪态、执行态、等待态(阻塞态)


就绪态

:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。


执行态

:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。


阻塞态

:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。


2)进程调度(抢占式、非抢占式)



①先到先服务

②短进程优先

③时间片轮转

④高优先级优先


3)Linux进程状态

TASK_RUNNING:就绪态、可执行态

TASK_INTERRUPTABLE:进程被挂起(睡眠),知道等待条件为真才被唤醒

TASK_UNINTERRUPTABLE:深度睡眠,睡眠期间不响应引号

TASK_STOPPED:进程的执行被暂停

TASK_TRACED:被其他进程跟踪,常用于调试

EXIT_ZOMBIE:僵死状态,进程的执行被终止

EXIT_DEAD:僵死撤销状态,防止wait类系统调用的竞争状态发生


b)操作系统的核心就是任务(进程)管理


c)进程调度器:


1)进程分类


①处理器消耗型

渴望获得更多CPU时间,并消耗掉调度器分配的全部时间片

常见例子:

无限死循环、科学计算、影视特效渲染


②I/O消耗型

由于等待某种资源通常处于阻塞状态,不需要较长的时间片

常见例子:

等待用户输入|GUI程序、文件读写I/o程序


2)Linux调度策略

对不同进程采取不同调度策略、实现多个调度器:


完全公平调度CFS


实时进程调度RT


最终期限调度DL


IDLE类型调度



STOP类调度器

不同进程由不同的调度器管理,彼此之间互不干扰


处理器消耗类型进程

:减少优先级、分配尽可能长的时间片


I/O消耗进程

:增加优先级、增加实时性、增强用户体验


3)Linux进程优先级

进程的NICE值和优先级:


$nice -n 5 top

NI值越小,优先级越高。


d)进程同步

进程是并发执行的,不同进程之间存在着不同的相互制约关系。所谓进程同步(线程同步同理),主要是解决临界资源互斥访问的问题。如多个进程访问同一片共享内存,这片共享内存必须互斥使用。


临界资源

:操作系统中将一次只允许一个进程访问的资源称为临界资源,需要互斥访问


信号量实现互斥访问

7、进程创建 & 退出


a)进程的创建(fork函数):


1)fork函数

函数原型:


pid_t fork(void);

头文件:


#include <unistd.h>

函数功能:从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程。

函数返回值:fork调用一次,返回两次,可能三种不同值;

①父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。

②子进程的fork,成功返回0,失败返回-1,errno被设置。


2)exec函数族

执行一个二进制程序文件(execvp函数)

函数原型:

int execvp(const char *file, char *const argv[]);

参数1:要执行程序的名称

参数2:要执行文件的参数列表,参数列表以NULL指针为结束标记

函数作用:将当前进程的代码使用file程序文件代替并执行

返回值:

成功:无返回值

失败:返回-1,errno被设置


3)vfork函数


写时复制

:创建子进程后若对父进程复制过来的数据不做修改时,只会和父进程数据共享同一个空间,但需要修改时,子进程才会开辟自己的空间(节省空间)

函数原型:


pid_t vfork(void)

;

头文件:

#include <sys/types.h>

#include <unistd.h>

系统调用vfork:

①子进程共享父进程的代码、数据、堆栈资源

②使用vfork后,直接运行exec,节省了资源拷贝的时间

③使用vfork,创建子进程后直接运行子进程、父进程被阻塞

使用vfork创建子进程的退出:vfork创建的子进程共享父进程的代码、数据、堆栈资源,子进程退出时使用_exit/exit,使用return会破坏父进程的堆栈环境,产生段错误。

父进程退出一般使用exit默认子进程退出用_exit


4)system函数



函数原型:


int system(const char *command);

头文件:


#include <stdlib.h>


b)进程的退出:


1)exit库函数

函数原型:


void exit(int status);

函数参数:

0:表示正常退出

1/-1:表示进程退出异常

2^n:用户可自定义

exit函数的作用:

调用退出处理程序(通过atexit、on_exit注册的函数)

刷新stdio流缓冲区

使用由status提供的值执行_exit系统调用函数


atexit/on_exit函数

退出处理函数,在exit退出后可以自动执行

用户注册的退出处理程序

执行顺序与注册顺序相反

函数原型:


int atexit(void (*function)(void));

函数原型:


int on_exit(void (*function)(

int,


void *

), void *arg);


int:

exit(int)中的int

void *:void *arg

exit_group函数:

函数原型:


void exit_group(int status);


2)_exit函数

_exit执行流程:

①关闭进程打开的文件描述符、释放该进程持有的文件锁

②关闭该进程打开的信号量、消息队列

③取消该进程通过mmap()创建的内存映射

④将该进程的所有子进程交给init托管

⑥给父进程发送一个SIGCHLD信号

……

exit VS _exit

exit是库函数对_exit系统调用的封装

在调用_exit前,会执行各种操作:

①调用退出处理程序(通过atexit、on_exit注册的函数)

②刷新stdio流缓冲区

使用由status提供的值执行_exit系统调用函数

在一个进程中,直接调用_exit终止进程,缓冲区的数据可能会丢失1:要执行程序的名称参数1:要执行程序的名称

再创建子进程的应用中,只应有一个进程(一般为父进程)调用exit终止,而其他进程应调用_exit终止。从而确保了只有一个进程调用退出处理程序并刷新stdio缓冲区

如果一个进程使用atexit/on_exit注册了退出管理程序,则应使用exit终止程序的运行,否则注册的回调函数无法执行


3)退出方式

①正常退出:main调用return

②异常退出:

任意地方调用_exit函数

任意地方调用exit胡函数

被信号杀死

调用abort函数

8、进程等待


a)回收进程资源:

1)进程运行终止后,不管进程是否正常终止,都必须回收进程所占用的资源

2)如何查看进程资源:ps命令(在进程PID中)

3)为什么要回收进程的资源?

进程占用资源,运行效率下降,为提高运行效率,所以要回收进程资源。

4)父进程运行结束后,只负责回收子进程资源

5)./a.out进程的父进程是谁?

①PID==0进程:调度进程,实现进程间的调度和切换(PC指向不同进程时,CPU执行不同程序)

②PID==1进程:Init进程,是操作系统完全在硬件上运行起来后启动的第一个程序就是Init进程。它有三个功能:读取系统文件,与用户交互;托管孤儿进程;原始父进程。

③PID==2进程:页精灵进程,专门负责虚拟内存的请页操作。

6)僵尸进程和孤儿进程


僵尸进程

:是子进程已经终止不再运行,而父进程还在运行,父进程没有释放子进程占用的资源。(子进程站着茅坑不**,它爹父进程工作结束下班后,给子进程擦屁股收拾资源)


孤儿进程

:是父进程终止不再执行,而子进程还在运行。(没爹没妈,被扔进托管所Init,Init给孤儿进程擦屁股收拾资源)

为了回收孤儿进程终止之后的资源,孤儿进程会被托管给Init进程,当被托管的子进程终止时,Init会立即主动回收孤儿进程的资源,并且回收速度很快,不会让孤儿进程变成僵尸进程。


b)wait函数:

函数原型:


pid_wait(int *status);



头文件:


#include <sys/types.h>


#include <sys/wait.h>

参数:子进程调用exit/_exit时的status

函数功能:

等待子进程的终止及信息

函数返回值:

成功:返回已终止子进程的pis

失败:返回-1,errno被设置

注:若子进程没有被终止,wait调用会阻塞父进程,直到子进程终止,子进程终止后,该调用立刻返回

子进程的返回状态:


c)waitpid函数:

函数原型:


pid_wait(pid_t pid,int *status,int options);

头文件:


#include <sys/types.h>


#include <sys/wait.h>

参数:


pid:

要等待的进程PID号

pid < -1 等待pid绝对值的子进程

pid = -1 等待任何子进程,相当于wait();

pid = 0 等待与目前进程相同的任何子进程

pid > 0 等待任何子进程识别码为pid的子进程


status:

子进程调用exit/_exit时的status,可设置为NULL。


options:

有三种状态,

0



WNOHANG



WUNTRACED


0

:死等


WNOHANG

:如过没有任何已经结束的子进程则立即返回,不等待。(看一眼,如果没有结束的子进程,就直接返回,不阻塞父进程)。


WUNTRACED

:如果子进程进入暂停执行情况则立即返回,但结束状态不理会。

函数功能:

等待特定子进程的终止及信息

函数返回值:

成功:返回已终止子进程的pis

失败:返回-1,errno被设置

注:若子进程没有被终止,waitpid调用会阻塞父进程,直到子进程终止,子进程终止后,该调用立刻返回子进程结束状态值

waitpid()可以用来解决僵尸进程,waitpid(pid, NULL,WNOHANG);若进程结束这回收子进程资源。

对于进程的理解:

进程是程序的抽象,把所有的程序都统一按照进程来进行管理,因为程序有不同的大小、不同的资源占用,所以都统一成进程来管理。

进程是资源分配的最小单位,因为所有程序都被抽象成为一个一个的进程,操作系统分配资源时,都是按照进程为单位分配的。

每个进程都有独立的运行地址空间,这就会使进程与进程之间互不干扰,一个进程的消亡,不会影响到其他进程,所以它是一种安全的任务机制。但是给每个进程分配独立的地址空间这种多任务机制也暴露出了它的缺点,它的开销比较大,因为每个进程都会占用一个独立的地址空间。

进程的调度是由内核的调度算法,调度算法实际上是前期把进程划分了不同状态,在用户层有三态,分别是执行态、就绪态和阻塞态,就绪→执行:处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态;执行→就绪:处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态;执行→阻塞:正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。在三态下建立了一些基本策略,先到先服务,时间片轮转,和高优先级优先。进程的调度算法有CFS、idul,这些都是内核功能的调度算法,这是内核实现的,我们在用户层只掌握一个功能,那就是进程的创建。

Linux提供了四种创建进程的方式,fork、vfork、exec函数族和system,fork是子进程拷贝父进程的数据段,子进程的执行次序不定,而vfork是子进程与父进程共享数据段,是子进程先执行,父进程后执行,fork的工作原理是给进程复制一个一模一样的空间,vfork在调用exec之前,都是共享的父进程的空间,他执行的是一种写时复制,内核里面也是写时复制,exec函数族和system都是调用另一个程序。

进程间退出分为正常退出和异常退出,正常退出是return 0,异常退出就是exit、_exit、被信号杀死,或者是abort函数,exit是库函数,而_exit是系统调用,exit封装了_exit,解决了刷新缓冲区,还解决了可以做特殊处理程序。

在进程的执行中可能产生僵尸进程和孤儿进程,因为子进程的资源是要靠父进程回收的。僵尸进程是父进程在执行,子进程退出,但父进程没有回收子进程的资源,而孤儿进程是父进程退出,子进程还在执行。避免僵尸进程的方式有三种,第一种是通过信号机制,在处理函数中调用wait回收资源;第二种是fork两次,将子进程变为孤儿进程,从而使其父进程变为init进程,让init进程回收资源;第三种调用wait和waitpid,让父进程等待子进程退出,使父进程挂起,没有起到多进程的作用,同时浪费资源。由于孤儿进程是父进程退出,子进程还在执行,就将子进程托孤给init,让其充当子进程的父进程,并回收资源。



版权声明:本文为qq_47615588原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。