【北京迅为】iTOP-4412全能版使用手册-第二十四章 进程创建与回收
iTOP-4412全能版采用四核Cortex-A9,主频为1.4GHz-1.6GHz,配备S5M8767 电源管理,集成USB HUB,选用高品质板对板连接器稳定可靠,大厂生产,做工精良。接口一应俱全,开发更简单,搭载全网通4G、支持WIFI、蓝牙、陀螺仪、CAN总线、RS485总线、500万摄像头等模块,稳定运行Android 4.0.3/Android 4.4操作,系统通用Linux-3.0.15+Qt操作系统(QT支持5.7版本),Ubuntu版本:12.04,接口智能分配 方便好用。
第二十四章 进程创建与回收
24.1 本章导读
在 linux 中“文件”应该是最重要的抽象概念,从前面给大家准备的例程就可以看出来, 非常的多。仅次于“文件”的概念就是 linux 进程,本章介绍进程的一些基础概念以及简单的小例程。
本章配套视频为:
“视频 16_01linux 进程之进程基本概念”
“视频 16_02linux 进程之进程 ID”
“视频 16_03linux 进程之 exec 函数族”
“视频 16_04linux 进程之 fork 创建新进程”
“视频 16_05linux 进程之综合例程”
相信大多数人学习嵌入式都是从单片机开始学起的,有些单片机教程不严谨,给大家灌输一些偏颇的概念“单片机和 arm 一样”“学习了单片机编程,操作系统也是一样的”等等。
其实完全不一样!
中间隔了一层操作系统,也就是一层软件,嵌入式 Linux 系统很少需要开发者直接对寄存器操作,即使对寄存器的操作,也是移植,不会让你配置很多的寄存器。这部分可以理解为驱动移植也就是类似对硬件的操作,但是思路完全不一样了。
单片机上面的编程,一般被称为‘裸机程序’,也就是说我们在上面写个程序直接跑到硬件上面,并没有驱动程序和上层应用的概念,说直白些就是胡子眉毛一把抓,所有的程序都在自己写的代码里;这样会给我们有种掌控感,包括对寄存器的操作,中断函数都要自己写出 来,心里也会清晰明了。
但是,我们在做大型应用的时候,会用到很多技术,比如网络协议栈,图形系统等,这个时候再用‘裸机’就显得力不从心了,我们就需要在 Linux 架构的基础上来完成。
Linux 是个庞大的软件,很少人能把所有的代码看一遍,而且这样做也没有必要。这个时候就必须把一些概念‘抽象’出来,也就是说,在抽象的层次上去编程。我们需要学习 Linux 的架构和规范,在 Linux 框架的基础上进行编程。这时,很多单片机工作时养成的编程习惯就不适用了,很多人觉得会没有‘底气’,失去了裸机程序的掌控感,从而影响入门。
所以,我们必须要适应这种编程心态的改变,学会在操作系统基础上编程和做产品的思路与习惯。
几个基本概念
什么是‘程序’呢?科学的定义就是编译过的,可执行的二进制代码,这个很好理解。如果程序很大,可以叫做应用,这里提到的‘程序’以及‘应用’都是类似的概念。
‘进程’是指正在运行的程序,一个程序中可以包含多个进程;而一个进程可能包含一个或者多个‘线程’。
如下图所示,执行一下命令“top”。
接着看一下 x86-Ubuntu 系统的,下面的 pid 会不时的刷新变化。
如下图所示,是开发板最小系统的进程。
24.2 实验-进程ID
进程 id 基本概念
每一个进程都有一个唯一的标识符,进程 ID 简称 pid。
进程的 ID 在一个固定的时刻是唯一的,需要注意的是,假如你在 s 秒的时候有一个进程ID 是 1000,在 另外一个时刻 s+n,另一个进程 ID 也有可能是 1000。
另外内核运行的第一个进程是 1,也就是内核的 init 程序,这个是唯一的。
进程 id 一般默认的最大值为 32768,不过也是可以修改的,当然一般情况下不需要这么做。如果当前进程是 1000,那么下一个分配的进程就是 1001,它是严格线性分配的。直到pid 到了最大值,才重新分配已经用过的进程 ID,当然这些进程都是已经死亡的进程。
除了 init 进程,其它进程都是由其它进程创立的。创立新进程的进程叫父进程,新进程叫子进程。
使用‘man’命令学习 getpid 和 getppid
如下图所示,使用命令“man 2 getpid”。
如下图所示,有两个类似的函数 getpid 和 getppid。
接着注意一下相关的函数,如下图所示,下面红框中的函数部分会在教程中介绍。
接着介绍一下 getpid 和 getppid 的用法。
pid_t getpid(void);
参数:无。
返回值:成功返回进程号。
pid_t getppid(void);
参数:无。
返回值:成功返回父进程。
例程
编写简单的 getpid.c 文件测试 getpid 和 getppid 函数。首先添加头文件,如下图所示。
然后 main 函数如下图所示
如上图代码所示。
第 10-12 行:获取子进程,没有则为 0。
第 13-16 行:获取父进程,前面提到过除了内核中 init 进程,其它进程都有父进程。
编译运行测试
在 Ubuntu 系统下,如下图所示,进入前面实验创建的目录“/home/linuxsystemcode”,使用命令“mkdir pid”新建 pid 文件夹,将源码 getpid.c 拷贝进去,进入新建的文件夹 pid,如下图所示。
使用命令“arm-none-linux-gnueabi-gcc -o getpid getpid.c -static”编译 getpid 文件,如下图所示,使用命令“ls”可以看到生成了 getpid 可执行文件。
这里介绍通过 U 盘拷贝来运行代码的方法,也可以编译进文件系统或者使用 NFS 文件系统等等。
将编译成的可执行文件 getpid,拷贝到 U 盘,启动开发板,插入 U 盘,加载 U 盘,运行程序如下。
如上图所示,pid927 是当前程序的进程号,pid1209 是当前进程的父进程。
查看当前程序的进程
Linux 中有进程配套的命令 ps 个 kill,可以查看和终止进程。如下图所示,ps 命令查看进程号。
如果是一个循环程序,执行到 while(1),运行的时候可以在执行命令后面添加&,后台运行,然后使用 ps 查询 id 号,使用 kill 命令结束进程。
24.3 实验-exec函数族
在学习创建进程之前,先来学习一下 linux 中重要的 exec 函数族。在 linux 中,exec 函数族是把程序直接载入内存,而不是在一个程序中运行多个进程。
如上图所示,最简单直白的解释就是 exec 函数族调用成功之后,会在内存中执行一个新的程序。在 linux 中要运行多任务需要使用 exec 函数族和 fork 进程(下一小节的内容)。
使用‘man 命令’学习 exec 函数
如下图所示,使用命令“man 3 exec”。
如下图所示,有六个类似的函数 execl, execlp, execle, execv, execvp, execvpe 。
接着注意一下相关的函数,如下图所示。其中 fork 进程调用函数也是非常重要的,将在下一小节介绍。
exec 函数族比较多,但是却很容易记忆和区分的,下面给大家总结一下它们的区别。
如上图所示,首先函数族都是以 exec+xx 的方式命名的。
“l”和“v”表示参数是以列表还是以数组的方式提供的。
“p”表示这个函数的第一个参数是*path,就是以绝对路径来提供程序的路径,也可以以当前目录作为目标。
“e”表示为程序提供新的环境变量。
exec 函数族例程
编写简单的 exec.c 文件测试 exec 函数。首先添加头文件,如下图所示。
然后 main 函数如下图所示。
如上图代码所示。
第 1-4 行:头文件
第 8-12 行:使用 execl 函数执行“/mnt/udisk/”目录下的 helloexec 程序,传递参数helloexec 和 execl。
第 13-15 行:运行成功则不会执行,不正常则打印 execl error!。
接着编写简单的 helloexec.c 文件,如下图所示。
如上图所示,main 函数的第二个参数传递进入之后,argv[1]将被打印出来,如果正常执行则会打印“Hello execl!”。
编译运行测试
在 Ubuntu 系统下,如下图所示,进入前面实验创建的目录“/home/linuxsystemcode”,使用命令“mkdir exe”新建 exe 文件夹,将源码 execl.c 和helloexec.c 拷贝进去,进入新建的文件夹 exe,如下图所示。
使用命令“arm-none-linux-gnueabi-gcc -o execl execl.c -static”和“arm-none- linux-gnueabi-gcc -o helloexec helloexec.c -static”编译 execl 和 helloexec 文件,如下图所示,使用命令“ls”可以看到生成了 helloexec 和 execl 可执行文件。
这里介绍通过 U 盘拷贝来运行代码的方法(可参考 10.3.5 小节),也可以编译进文件系统或者使用 NFS 文件系统等等
将编译成的可执行文件 execl 和 helloexec,拷贝到 U 盘,启动开发板,插入 U 盘,加载U 盘,运行程序如下。
如上图所示,可以看到执行了 helloexec 的打印函数,调用成功。
24.4 实验-fork创建新进程
在 linux 中可以使用 fork 创建和当前进程一样的进程,新的进程叫子进程,原来的进程叫父进程。
使用 man 学习 fork 进程
如下图所示,使用命令“man 2 fork”。
如下图所示,函数 fork。
接着注意一下相关的函数,如下图所示。
接着介绍一下 fork 的用法。
pid_t fork(void);
参数:无
返回值:执行成功,子进程 pid 返回给父进程,0 返回给子进程;出现错误-1,返回给父进程。执行失败的唯一情况是内存不够或者 id 号用尽,不过这种情况几乎很少发生。
进程函数 fork 的返回值
系统函数 fork 调用成功,会创建一个新的进程,它几乎会调用差不多完全一样的 fork 进程。
子进程的 pid 和父进程不一样,是新分配的。
子进程的 ppid 会设置为父进程的 pid,也就是说子进程和父进程各自的“父进程”不一样。
子进程中的资源统计信息会清零。
挂起的信号会被清除,也不会被继承(后面章节进程通信中会介绍信号)。所有文件锁也不会被子进程继承。
这里有一个很难理解的地方是,fork 函数的返回值问题。
fork 函数执行成功,子进程 pid 返回给父进程,0 返回给子进程;
出现错误-1,返回给父进程。执行失败的唯一情况是内存不够或者 id 号用尽,不过这种情况几乎很少发生。
函数例程
编写简单的 fork.c 文件测试 fork 函数。首先添加头文件,如下图所示。
main 函数,部分代码如下图所示。
main 函数,如下图所示。
如上图代码所示。
第 1-4 行:头文件。
第 6-9 行:定义变量。
第 10-27 行:调用 fork 函数。如果出错返回-1,打印 fork failed!;如果成功,则给父进程返回子进程号,同时给子进程返回父进程号。
编译运行测试
在 Ubuntu 系统下,如下图所示,进入前面实验创建的目录“/home/linuxsystemcode/exe”,将源码 fork.c 拷贝进去,如下图所示。
使用命令“arm-none-linux-gnueabi-gcc -o fork fork.c -static”编译 fork 文件,如下图所示,使用命令“ls”可以看到生成了 fork 可执行文件。
这里介绍通过 U 盘拷贝来运行代码的方法,也可以编译进文件系统或者使用 NFS 文件系统等等
将编译成的可执行文件 fork,拷贝到 U 盘,启动开发板,插入 U 盘,加载 U 盘,运行程序如下。
如上图所示,可以看到打印的父进程和子进程号,然后其中打印的 i 都是一样的。子进程可以使用父进程中定义的变量,和父进程中的变量却是不同的变量。
进程的调用
生成进程的 fork 函数对于初学者是个难点,看上去它的行为不符合常规,而这也正好体现了操作系统多任务的特点。
网上关于进程这部分的博文,也有很多人提出疑问,如下图所示。
还有类似下面疑惑。
其实在介绍很多知识点的时候,用单片机或者裸机的概念去理解都是没有太多问题的,但是到了这里,很容易出现一些无法理解的情况。
对于初学者来讲,理解‘fork’函数是比较困难的,因为 fork 以后程序会分岔,会沿着两条不同的路径去执行,这跟我们以往‘程序是顺序执行的’这样的思想似乎有点相悖。实际上,这个函数执行以后系统会生成另一个进程,新的进程会和父进程并列,并接受时间片的调度。说到底,程序仍然是在顺序执行的。
24.5 实验-进程终止exit
这个函数大家肯定已经用过很多次了,就是 exit,在 main 函数的结尾会使用 return 或者 exit 结束程序。当使用 exit 的时候,就是使用的进程终止函数 exit。
使用 man 学习 exit 函数
如下图所示,使用命令“man 2 exit”。
如下图所示,有一些相关的函数 execve(2), exit_group(2), fork(2), kill(2), wait(2)等等。
接着介绍一下 void exit(int status)的用法。
void exit(int status)
参数:返回给父进程的参数。
返回值:无。
函数_exit 和 exit 没什么区别,不需要花太多时间去追究。
创建进程还有一个 vfork 函数,感兴趣的可以通过网络找个例子实现以下,vfork 的使用会产生很多新问题,使用 vfork 创建的进程结束需要调用_exit。
Linux 命令的本质
在这一段时间的学习中,Linux 命令大家肯定用的比较熟练了,不会再有陌生的感觉了。其实这些命令都是一些小程序,大家感兴趣也可以看看这些小程序的源码。
exec 函数族可以执行新程序,那么肯定也能运行这些“Linux 命令”小程序。
24.6 实验exec函数族综合例程
有了 exec 函数族基础知识,为了加深大家对 linux 命令和 exec 函数族的理解,结合起来给大家提供一个小例程。
编写简单的 execls.c 文件。
首先添加头文件,如下图所示。
main 函数部分代码,如下图所示。
main 函数部分代码,如下图所示。
main 函数部分代码,如下图所示。
main 函数部分代码,如下图所示
如上图代码所示。
第 1-5 行:头文件
第 6-76 行:分别使用 exec 函数族实现运行 ls 命令的功能
编译运行测试
在 Ubuntu 系统下,如下图所示,进入前面实验创建的目录“/home/linuxsystemcode/exe”,将源码 execls.c 拷贝进去,进入新建的文件夹 exe,如下图所示。
使用命令“arm-none-linux-gnueabi-gcc -o execls execls.c -static”编译 execls 文件,如下图所示,使用命令“ls”可以看到生成了 open 可执行文件。
这里介绍通过 U 盘拷贝来运行代码的方法(可参考 10.3.5 小节),也可以编译进文件系统或者使用 NFS 文件系统等等
将编译成的可执行文件 execls,拷贝到 U 盘,启动开发板,插入 U 盘,加载 U 盘,运行程序如下。
先把 execls 文件拷贝到当前目录下,使用 ls 命令,查看当前目录,便于和后面执行程序之后对比。
运行后部分结果如下图所示。
如上图所示,可以和前面执行 ls 命令之后对比。