描述
开 本: 16开纸 张: 胶版纸包 装: 平装是否套装: 否国际标准书号ISBN: 9787121280757
图文结合视频,全面精讲Linux编程关键知识点;
只要你看,就能懂能会能用。
本书从零开始,循序渐进地攻破Linux环境编程所遇到的各级关卡,以图文并茂的形式帮助读者理解各个概念。本书内容翔实,囊括了Linux系统操作细节,Shell脚本编程精要,各种编程环境所需要解决的技术难点,以及在Linux环境下的C语言编程技术、并发编程技术和音/视频编程等核心内容。全书用400余幅图表帮助读者理解复杂概念,因此读者不需要具备任何计算机编程经验,在本书的指导下就能进入编程的世界,并能在阅读和实践中享受编程的乐趣。同时,本书配套完整的视频教程,给读者以直观、容易吸收知识的方式,融会贯通书中所有的知识点。不仅如此,读者还能够得到作者及其团队的在线技术支援和答疑。
本书通俗易懂,适合从事Linux/UNIX编程开发、嵌入式开发、C环境开发的读者,尤其适合计算机相关专业的高职院校的学生,以及希望转向IT类就业方向的在职人士。
第1章 Linux编程环境 1
1.1 基本工具 1
1.1.1 免费大餐:Ubuntu 1
1.1.2 桌面系统:gnome 6
1.1.3 网络配置:纯手工打造 6
1.1.4 软件集散地:APT 8
1.1.5 无敌板斧:vi 10
1.1.6 开发圣典:man 13
1.1.7 配置共享目录 15
1.2 Shell命令 17
1.2.1 概念扫盲 17
1.2.2 命令详解 19
1.2.3 上古神器 38
1.3 Shell脚本编程 45
1.3.1 开场白 45
1.3.2 脚本格式 45
1.3.3 变量 46
1.3.4 特殊符号们 48
1.3.5 字符串处理 50
1.3.6 测试语句 51
1.3.7 脚本语法单元 52
1.4 编译器:GCC 55
1.4.1 简述 55
1.4.2 编译过程简介 55
1.4.3 实用的编译选项 58
1.5 解剖Makefile 59
1.5.1 工程管理器make 59
1.5.2 概览性示例 60
1.5.3 书写格式 60
1.5.4 变量详解 62
1.5.5 各种规则 71
1.5.6 条件判断 75
1.5.7 函数 77
1.5.8 实用make选项集锦 85
1.6 GNU-autotools 86
1.6.1 autotools简介 86
1.6.2 文件组织 87
1.6.3 configure.ac编写规则 88
第2章 深度Linux-C 92
2.1 基本要素 92
2.1.1 Linux下C代码规范 93
2.1.2 基本数据类型 97
2.1.3 运算符 108
2.1.4 控制流 116
2.2 函数 124
2.2.1 函数初体验 125
2.2.2 函数调用内幕 128
2.2.3 递归思维及其实现 130
2.2.4 变参函数 133
2.2.5 回调函数 137
2.2.6 内联函数 140
2.3 数组与指针 142
2.3.1 数组初阶 142
2.3.2 内存地址 144
2.3.3 指针初阶 145
2.3.4 复杂指针定义 147
2.3.5 指针运算 151
2.3.6 数组与指针 152
2.3.7 复杂数组剖析 155
2.3.8 const指针 158
2.3.9 char指针和char数组 160
2.4 内存管理 162
2.4.1 进程内存布局 162
2.4.2 堆(Heap) 164
2.5 组合数据类型 167
2.5.1 结构体 167
2.5.2 共用体 171
2.5.3 枚举 172
2.6 高级议题 173
2.6.1 工程代码组织 173
2.6.2 头文件 175
2.6.3 宏(macro) 176
2.6.4 条件编译 182
2.6.5 复杂声明 184
2.6.6 attribute机制 185
第3章 Linux的数据组织 188
3.1 无所不在的链表 188
3.1.1 开场白 188
3.1.2 单向链表 190
3.1.3 单向循环链表 198
3.1.4 双向循环链表 200
3.1.5 Linux内核链表 210
3.2 线性表变异体 227
3.2.1 堆叠的盘子:栈 227
3.2.2 文明的社会:队列 236
3.3 小白慎入:非线性结构 243
3.3.1 基本概念 243
3.3.2 玩转BST 247
3.3.3 各种的遍历算法 260
3.3.4 自平衡AVL树 263
3.3.5 自平衡Linux红黑树 273
第4章 I/O编程技术 289
4.1 一切皆文件 289
4.1.1 文件的概念 289
4.1.2 各类文件 290
4.2 文件操作 290
4.2.1 系统I/O 291
4.2.2 标准I/O 306
4.2.3 文件属性 320
4.3 目录检索 327
4.3.1 基本概念 327
4.3.2 相关API 328
4.4 触控屏应用接口 330
4.4.1 输入子系统简介 330
4.4.2 TSLIB库详解 333
4.4.3 划屏算法 338
第5章 Linux进程线程 345
5.1 Linux进程入门 345
5.1.1 进程概念 345
5.1.2 进程组织方式 346
5.2 进程的“生老病死” 348
5.2.1 进程状态 348
5.2.2 相关重要API 350
5.3 进程的语言 358
5.3.1 管道 358
5.3.2 信号 363
5.3.3 system-V IPC简介 380
5.3.4 消息队列(MSG) 381
5.3.5 共享内存(SHM) 387
5.3.6 信号量(SEM) 392
5.4 Linux线程入门 400
5.4.1 线程基本概念 400
5.4.2 线程API及特点 401
5.5 线程安全 410
5.5.1 POSIX信号量 410
5.5.2 互斥锁与读写锁 415
5.5.3 条件变量 418
5.5.4 可重入函数 421
5.6 线程池 422
5.6.1 实现原理 422
5.6.2 接口设计 423
5.6.3 实现源码 425
第6章 Linux音频、视频编程 433
6.1 基本背景 433
6.2 Linux音频 433
6.2.1 音频概念 433
6.2.2 标准音频接口ALSA 436
6.3 Linux视频输出 450
6.3.1 基本概念 450
6.3.2 framebuffer 452
6.3.3 在LCD上画图 462
6.3.4 效果算法 469
6.4 Linux视频输入 478
6.4.1 V4L2简介 478
6.4.2 V4L2视频采集流程 478
6.4.3 V4L2核心命令字和结构体 481
6.4.4 编码格式和媒体流 484
6.5 多媒体开发库SDL 489
6.5.1 SDL简介 489
6.5.2 编译和移植 489
6.5.3 视频子系统 490
6.5.4 音频子系统 494
6.5.5 事件子系统 498
6.5.6 处理YUV视频源 502
6.6 音/视频编解码库FFmpeg 504
6.6.1 FFmpeg简介 504
6.6.2 核心结构体与常用API 505
6.6.3 与SDL结合实现简单的播放器 511
前 言
本书定位Linux环境编程入门与提高,全书配送近百个教学视频,400余幅案例图表,200多篇源代码,力争做到图文并茂。作为粤嵌教育的专业教员,我和我的同事们都深刻地认识到,很多编程初入行的朋友成长曲线平缓,不是因为概念和原理有多复杂,而是很多教程和图书没有将原理用容易理解的图画表现出来,所谓一图顶万言,讲的就是这个道理。基于这样的认识,粤嵌教育教材研发中心的同事们几乎对每一个概念都力争用图画的形式来表现,因此本书的出版和面世也迟缓很多,但我们认为这是值得的。
本书面向的读者人群,是所有希望从事Linux/UNIX编程开发、嵌入式开发、C环境开发的朋友,尤其适合计算机相关专业的高职院校的毕业生,以及希望转向IT类就业方向的在职人士,阅读本书不需要掌握任何专门的计算机技术和编程经验,但是对计算机的运行原理需要有一定认知。当然,学习过任何一门编程语言将使读者在阅读和学习本书的内容时更能稳操胜券。
本书的作者和审校同事都是长期从事培训教育行业的一线培训工作者,购买本书的同时实际上也加入了由广州粤嵌教育主导的IT技术学习圈子,可以登录了解更多资讯。
本书共分6章,按照从易到难的路径顺序讲述。
第1章着重介绍整个Linux的编程环境,包括如何安装Linux系统,以及如何使用Shell来操作用户的系统,本章还详细介绍了Linux下编程的三大技能,Shell脚本编程、Makefile语法和GNU开源开发套件autotools的详细使用方法。
第2章深度剖析C语言,大量使用图文方式解释内存机制,从根本上解决初学者对内存认识不到位的问题,具体而真实地掌握内存是学好编程的一大秘诀。另外,本章还介绍了Linux下的C语言的一些扩展增强语法。
第3章讲解数据组织结构,并且联系Linux内核使用实况详细剖析了传统链表、内核链表、栈和队列、二叉搜索树以及内核红黑树等高级数据结构,全章图文并茂,一目了然,对于这些纯算法也能确保读者学习愉悦,不枯燥。
第4章讲解Linux文件I/O编程,详述标准I/O和系统I/O,图解包括触摸屏在内的特殊设备文件的操作,读者在学习完本章之后对Linux的文件管理、目录操作会有本质上的提升。
第5章全面介绍Linux并发编程中的核心技术,包括多进程、多线程、IPC、同步互斥等,全章同样图文并茂,确保每一个知识点都能在图画中得到解答。
第6章是Linux应用编程的高级部分,在前面章节的基础上着重介绍了跟Linux音/视频相关的概念和使用,详细剖析ALSA机制、framebuffer、V4L2机制、SDL和FFmpeg库的使用等,让读者可以编程实现在Linux系统和嵌入式系统中实现图片显示、声音录制、音乐播放、视频播放等内容。
作 者
2016年2月
第5章
Linux进程线程
5.1 Linux进程入门
5.1.1 进程概念
一个程序文件(Program),只是一堆待执行的代码和部分待处理的数据,它们只有被加载到内存中,然后让CPU逐条执行其代码,根据代码做出相应的动作,才形成一个真正“活的”、动态的进程(Process)。因此,进程是一个动态变化的过程,是一出有始有终的戏,而程序文件只是这一系列动作的原始蓝本,是一个静态的剧本。
图5-1更好地展示了程序和进程的关系。
图5-1 ELF文件与进程虚拟内存
图5-1中的程序文件,是一个静态的存储于外部存储器(如磁盘、flash等掉电非易失器件)之中的文件,里面包含了将来进程要运行的“剧本”,即执行时会被复制到内存的数据和代码。除了这些部分,ELF格式中的大部分数据与程序本身的逻辑没有关系,只是程序被加载到内存中执行时,系统需要处理的额外的辅助信息。另外注意.bss段,这里面放的是未初始化的静态数据,它们是不需要被复制的,具体解释请参阅2.4.1节。
当这个ELF格式的程序被执行时,内核中实际上产生了一个名为task_struct{}的结构体来表示这个进程。进程是一个“活动的实体”,这个活动的实体从一开始诞生就需要各种各样的资源以便于生存下去,比如内存资源、CPU资源、文件、信号、各种锁资源等,所有这些东西都是动态变化的,这些信息都被事无巨细地一一记录在结构体task_struct之中,所以这个结构体也常常称为进程控制块(Process Control Block,PCB)。
下面是该结构体的掠影。
vincent@ubuntu:~/Linux-2.6.35.7/include/Linux$ cat sched.h -n
……
1168 struct task_struct {
1169 volatile long state;
1170 void *stack;
1171 atomic_t usage;
1172 unsigned int flags; /* per process flags, defined below */
1173 unsigned int ptrace;
1174
1175 int lock_depth; /* BKL lock depth */
1176
1177 #ifdef CONFIG_SMP
1178 #ifdef __ARCH_WANT_UNLOCKED_CTXSW
1179 int oncpu;
1180 #endif
1181 #endif
1182
1183 int prio, static_prio, normal_prio;
1184 unsigned int rt_priority;
1185 const struct sched_class *sched_class;
1186 struct sched_entity se;
1187 struct sched_rt_entity rt;
……
如果没什么意外,这个结构体可能是的单个变量了,一个结构体就有好几KB那么大,想想它包含了一个进程的所有信息,这么庞大也就不足为怪了。Linux内核代码纷繁复杂、千头万绪,这个结构体是系统进程在执行过程中所有涉及的方方面面的缩影,包括系统内存管理子系统、进程调度子系统、虚拟文件系统等,以这个所谓的PCB为切入点,是一个很好的研究内核的窗口。
总之,当一个程序文件被执行时,内核将会产生这么一个结构体,来承载所有该活动实体日后运行时所需要的所有资源,随着进程的运行,各种资源被分配和释放,是一个动态的过程。
5.1.2 进程组织方式
既然进程是一个动态的过程,有诞生的一刻,也就有死掉的一天,跟人类非常相似,人不可能无父无母,不可能突然从石头中蹦出来,进程也一样,每一个进程都必然有一个生它的父母(除了init),这个父母是一个被称为“父进程”的进程。实际上可以用命令pstree来查看整个系统的进程关系。
vincent@ubuntu:~$ pstree
init─┬─NetworkManager───{NetworkManager}
├─accounts-daemon───{accounts-daemon}
├─acpid
├─at-spi-bus-laun───2*[{at-spi-bus-laun}]
├─atd
├─avahi-daemon───avahi-daemon
├─bluetoothd
├─colord───2*[{colord}]
├─console-kit-dae───64*[{console-kit-dae}]
├─cron
├─cupsd
├─3*[dbus-daemon]
├─2*[dbus-launch]
├─dconf-service───2*[{dconf-service}]
├─gconfd-2
├─geoclue-master
├─6*[getty]
├─gnome-keyring-d───6*[{gnome-keyring-d}]
├─gnome-terminal─┬─3*[bash]
│ ├─bash───pstree
│ ├─gnome-pty-helpe
│ └─3*[{gnome-terminal}]
├─goa-daemon───{goa-daemon}
├─gsd-printer───{gsd-printer}
├─gvfs-afc-volume───{gvfs-afc-volume}
├─gvfs-fuse-daemo───3*[{gvfs-fuse-daemo}]
├─gvfs-gdu-volume
├─gvfs-gphoto2-vo
├─gvfsd
├─gvfsd-burn
├─gvfsd-metadata
├─gvfsd-trash
……
pstree是一个用“树状”方式查看当前系统所有进程关系的命令,可以明显看到它们的关系就像人类社会的族谱,大家都有一个共同的祖先init,每个人都可以生出几个孩子(进程没有性别,自己一个人就能生!)。其中祖先init是一个非常特别的进程,它没有父进程!它是一个真正从石头(操作系统启动镜像文件)中蹦出来的野孩子。
另外,每个进程都有自己的“身份证号码”,即PID号,PID是重要的系统资源,它是用以区分各个进程的基本依据,可以使用命令ps来查看进程的PID。
vincent@ubuntu:~$ ps -ef | more
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Jul22 ? 00:00:03 /sbin/init
root 2 0 0 Jul22 ? 00:00:00 [kthreadd]
root 3 2 0 Jul22 ? 00:00:06 [ksoftirqd/0]
root 6 2 0 Jul22 ? 00:00:01 [migration/0]
root 7 2 0 Jul22 ? 00:00:01 [watchdog/0]
root 8 2 0 Jul22 ? 00:00:00 [migration/1]
root 10 2 0 Jul22 ? 00:00:05 [ksoftirqd/1]
root 11 2 0 Jul22 ? 00:00:02 [watchdog/1]
root 12 2 0 Jul22 ? 00:00:00 [cpuset]
root 15 2 0 Jul22 ? 00:00:00 [netns]
root 17 2 0 Jul22 ? 00:00:01 [sync_supers]
……
上述信息中的第2列就是PID,而第3列是每个进程的父进程的PID。既然进程有父子关系,进程可以生孩子,那么自然会有“生老病死”,欲知后事如何,且听下节分解。
5.2 进程的“生老病死”
5.2.1 进程状态
说进程是动态的活动的实体,指的是进程会有很多种运行状态,一会儿睡眠、一会儿暂停、一会儿又继续执行。如图5-2所示为Linux进程从被创建(生)到被回收(死)的全部状态,以及这些状态发生转换时的条件。
图5-2 Linux进程状态转换图
结合图5-2所示,一起看一下进程从生到死的过程。
(1)从“蛋生”可以看到,一个进程的诞生,是从其父进程调用fork( )开始的。
(2)进程刚被创建出来时,处于TASK_RUNNING状态,从图5-2中可以看到,处于该状态的进程可以是正在进程等待队列中排队,也可以占用CPU正在运行,我们习惯上称前者为“就绪态”,后者为“执行态”。当进程状态为TASK_RUNNING并且占用CPU时才是真正运行。
(3)刚被创建的进程都处于“就绪”状态,等待系统调度,内核中的函数sched( )称为调度器,它会根据各种参数来选择一个等待的进程去占用CPU。进程占用CPU之后就可以真正运行了,运行时间有个限定,比如20ms,这段时间称为time slice,即“时间片”的概念。时间片耗光的情况下如果进程还没有结束,那么会被系统重新放入等待队列中等待。另外,正处于“执行态”的进程即使时间片没有耗光,也可能被别的更高优先级的进程“抢占”CPU,被迫重新回到等待队列中等待。
换句话说,进程跟人一样,从来都没有什么平等可言,有贵族就有屌丝,它们要处理的事情有不同的轻重缓急之分。
(4)进程处于“执行态”时,可能会由于某些资源的不可得而被置为“睡眠态/挂起态”,比如进程要读取一个管道文件数据而管道为空,或者进程要获得一个锁资源而当前锁不可获取,或者干脆进程自己调用sleep( )来强制自己挂起,这些情况下进程的状态都会变成TASK_INTERRUPIBLE或TASK_UNINTERRUPIBLE,它们的区别是一般后者跟某些硬件设置相关,在睡眠期间不能响应信号,因此TASK_UNINTERRUPIBLE的状态也称为深度睡眠,相应地TASK_INTERRUPIBLE期间进程是可以响应信号的。当进程所等待的资源变得可获取时,又会被系统置为TASK_RUNNING状态重新就绪排队。
(5)当进程收到SIGSTOP或SIGTSTP中的一个信号时,状态会被置为TASK_STOPPED,此时称为“暂停态”,该状态下的进程不再参与调度,但系统资源不释放,直到收到SIGCONT信号后被重新置为就绪态。当进程被追踪时(典型情况是被调试器调试时),收到任何信号状态都会被置为TASK_TRACED,该状态与暂停态是一样的,一直要等到SIGCONT才会重新参与系统进程调度。
(6)运行的进程跟人一样,迟早都会死掉。进程的死亡可以有多种方式,可以是寿终正寝的正常退出,也可以是被异常杀死。比如图5-2中,在main函数内return或调用exit( ),包括在后线程调用pthread_exit( )都是正常退出,而受到致命信号死掉的情况则是异常死亡,不管怎么死,后内核都会调用do_exit( )的函数来使得进程的状态变成所谓的僵尸态EXIT_ZOMBIE,单词ZOMBIE对于玩过“植物大战僵尸”的读者都不会陌生,这里的“僵尸”指的是进程的PCB(进程控制块)。
为什么一个进程的死掉之后还要把尸体留下呢?因为进程在退出时,将其退出信息都封存在它的尸体里面了,比如如果它正常退出,那退出值是多少呢?如果被信号杀死,那么是哪个信号呢?这些“死亡信息”都被一一封存在该进程的PCB当中,好让别人可以清楚地知道:我是怎么死的。
那谁会关心它是怎么死的呢?答案是它的父进程,它的父进程之所以要创建它,很大的原因是要让这个孩子去干某一件事情,现在这个孩子已死,那事情办得如何?孩子是否需要有个交代?但它又死掉了,所以之后将这些“死亡信息”封存在自己的尸体里面,等着父进程去查看。例如,父子进程可以约定:如果事情办成了退出值为0;如果权限不足退出值为1;如果内存不够退出值为2;等等。父进程可以随时查看一个已经死去的孩子的PCB来确定事情究竟办得如何。可以看到,在工业社会中,哪怕是进程间的协作,也充满了契约精神。
(7)父进程调用wait( ) /waitpid( )来查看孩子的“死亡信息”,顺便做一件非常重要的事情:将该孩子的状态设置为EXIT_DEAD,即死亡态,因为处于这个状态的进程的PCB才能被系统回收。由此可见,父进程应尽职尽责地及时调用wait( ) /waitpid( ),否则系统会充满越来越多的“僵尸”!
问题是,如何保证父进程一定要及时地调用wait( ) /waitpid( )从而避免僵尸进程泛滥呢?答案是不能,因为父进程也许需要做别的事情没空去帮那些死去的孩子收尸,甚至那些孩子在变成僵尸时,它的父进程已经先它而去了!
后一种情况其实比较容易解决:如果一个进程的父进程退出,那么祖先进程init(该进程是系统个运行的进程,它的PCB是从内核的启动镜像文件中直接加载的,不需要别的进程fork( )出来,因此它是无父无母的,系统中的所有其他进程都是它的后代)将会收养(adopt)这些孤儿进程。换句话说,Linux系统保证任何一个进程(除了init)都有父进程,也许是其真正的生父,也许是其祖先init。
而前一种情况是:父进程有别的事情要干,不能随时执行wait( ) /waitpid( )来确保回收僵尸资源。在这样的情形下,我们可以考虑使用信号异步通知机制,让一个孩子在变成僵尸时,给其父进程发一个信号,父进程接收到这个信号之后,对其进行处理,在此之前想干嘛就干嘛,异步操作。但即便是这样也仍然存在问题:如果两个以上的孩子同时退出变僵尸,那么它们就会同时给其父进程发送相同的信号,而相同的信号将会被淹没。如何解决这个问题,请参阅5.3.2节。
5.2.2 相关重要API
本节将详细展示进程开发相关的API,个需要知道的接口函数当然是创建一个新的进程,如表5-1所示。
表5-1 函数fork( )的接口规范
这个函数接口本身非常简单,简单到连参数都没有,但是这个函数有个与众不同的地方:它会使得进程一分为二!就像细胞分裂一样,如图5-3所示。
图5-3 细胞分裂
当一个进程调用fork( )成功后,fork( )将分别返回到两个进程之中,换句话说,fork( )在父子两个进程中都会返回,而它们所得到的返回值也不一样,如图5-4所示。
要着重注意如下几点。
(1)fork( )会使得进程本身被复制(想想细胞分裂),因此被创建出来的子进程和父进程几乎是一模一样的,说“几乎”意味着子进程并不是100%为一份父进程的复印件,它们的具体关系如下。
图5-4 创建子进程的过程示意图
父子进程的以下属性在创建之初完全一样,子进程相当于做了一份复制品。
l 实际UID和GID,以及有效UID和GID。
l 所有环境变量。
l 进程组ID和会话ID。
l 当前工作路径。除非用chdir()加以修改。
l 打开的文件。
l 信号响应函数。
l 整个内存空间,包括栈、堆、数据段、代码段、标准I/O的缓冲区等。
而以下属性,父子进程是不一样的。
l 进程号PID。PID是身份证号码,哪怕亲如父子,也要区分开。
l 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
l 挂起的信号。这些信号是所谓的“悬而未决”的信号,等待着进程的响应,子进程也不会继承这些信号。
(2)子进程会从fork( )返回值后的下一条逻辑语句开始运行。这样就避免了不断调用fork( )而产生无限子孙的悖论。
(3)父子进程是相互平等的。它的执行次序是随机的,或者说它们是并发运行的,除非使用特殊机制来同步它们,否则不能判断它们的运行究竟谁先谁后。
(4)父子进程是相互独立的。由于子进程完整地复制了父进程的内存空间,因此从内存空间的角度看它们是相互独立、互不影响的。
以下代码显示了fork( )的作用。
vincent@ubuntu:~/ch05/5.2$ cat fork.c -n
1 #include
2 #include
3
4 int main(void)
5 {
6 printf(“[%d]: before fork() … n”, (int)getpid());
7
8 pid_t x;
9 x = fork(); //生个孩子
10
11 printf(“[%d]: after fork() …n”, (int)getpid());
12 return 0;
13 }
执行效果如下。
vincent@ubuntu:~/ch05/5.2$ ./fork
[23900]: before fork() …
[23900]: after fork() …
vincent@ubuntu:~/ch05/5.2$ [23901]: after fork() …
可以看到,第11行代码被执行了两遍,函数getpid( )展示了当前进程的PID,其中23900是父进程,23901是子进程。从执行效果看还有一个很有意思的现象:子进程打印的信息被挤到Shell命令提示符(vincent@ubuntu:~/ch05/5.2$)之后!造成这个结果的原因是:Shell命令提示符默认会在父进程退出之后立即显示出来,而父进程退出之时,子进程还没来得及执行完第11行。
由于父子进程的并发性,以上程序的执行效果是不一定的,换句话说,们如果再执行一遍代码可能会得到这样的效果:
vincent@ubuntu:~/ch05/5.2$ ./fork
[23900]: before fork() …
[23901]: after fork() …
[23900]: after fork() …
vincent@ubuntu:~/ch05/5.2$
接下来一个脱口而出的疑问是:好不容易生了个孩子,但是干的事情跟父进程是一样的,那我们要这个孩子有何用呢?答案是:上述代码确实没有什么实际意义,事实上我们一般会让孩子去执行一个预先准备好的ELF文件或脚本,用以覆盖从父进程复制过来的代码,下面先介绍这个加载ELF文件或脚本的接口函数,如表5-2所示。
表5-2 函数族exec( )的接口规范
上述代码组成一个所谓的“exec函数簇”,因为它们都“长”得差不多,功能都是一样的,彼此间有些许区别(详见表5-2中的备注)。使用这些函数还要注意以下事实。
(1)被加载的文件的参数列表必须以自身名字为开始,以NULL为结尾。比如要加载执行当前目录下的一个名为a.out的文件,需要一个参数“abcd”,那么正确的调用应该是:
execl(“./a.out”, “a.out”, “abcd”, NULL);
或者:
const char *argv[3] = {“a.out”, “abcd”, NULL};
execv(“./a.out”, argv);
(2)exec函数簇成功执行后,原有的程序代码都将被指定的文件或脚本覆盖,因此这些函数一旦成功,后面的代码是无法执行的,它们也是无法返回的。
下面展示子进程被创建出来之后执行的代码,以及如何加载这个指定的程序。被子进程加载的示例代码如下。
vincent@ubuntu:~/ch05/5.2$ cat child_elf.c -n
1 #include
2 #include
3
4 int main(void)
5 {
6 printf(“[%d]: yep, I am the childn”, (int)getpid());
7 exit(0);
8 }
下面是使用exec函数簇中的execl来让子进程加载上述代码的示例。
vincent@ubuntu:~/ch05/5.2$ cat exec.c -n
1 #include
2 #include
3 #include
4
5 int main(int argc, char **argv)
6 {
7 pid_t x;
8 x = fork();
9
10 if(x > 0) //父进程
11 {
12 printf(“[%d]: I am the parentn”, (int)getpid());
13 exit(0);
14 }
15
16 if(x == 0) //子进程
17 {
18 printf(“[%d]: I am the childn”, (int)getpid());
19 execl(“./child_elf”, “child_elf”, NULL); //执行child_elf程序
20
21 printf(“NEVER be printedn”); //这是一条将被覆盖的代码
22 }
23
24 return 0;
25 }
下面是执行结果:
vincent@ubuntu:~/ch05/5.2$ ./exec
[24585]: I am the parent
vincent@ubuntu:~/ch05/5.2$ [24586]: I am the child
[24586]: yep, I am the child
从以上执行结果看到,父进程比其子进程先执行完代码并退出,因此Shell命令提示行又被夹在中间了,那么怎么让子进程先运行并退出之后,父进程再继续呢?子进程的退出状态又怎么传递给父进程呢?答案是:可以使用exit( )/_exit( )来退出并传递退出值,使用wait( )/waitpid( )来使父进程阻塞等待子进程,顺便还可以帮子进程收尸,这几个函数的接口如表5-3所示。
表5-3 函数exit()和_exit()的接口规范
以下代码展示了exit( )和_exit( )的用法和区别。
vincent@ubuntu:~/ch05/5.2$ cat exit.c -n
1 #include
2 #include
3 #include
4
5 void routine1(void) //退出处理函数
6 {
7 printf(“routine1 is called.n”);
8 }
9
10 void routine2(void) //退出处理函数
11 {
12 printf(“routine2 is called.n”);
13 }
14
15 int main(int argc, char **argv)
16 {
17 atexit(routine1); //注册退出处理函数
18 atexit(routine2);
19
20 fprintf(stdout, “abcdef”); //将数据输送至标准IO缓冲区
21
22 #ifdef _EXIT
23 _exit(0); //直接退出
24 #else
25 exit(0); //冲洗缓冲区数据,并执行退出处理函数
26 #endif
27 }
vincent@ubuntu:~/ch05/5.2$ gcc exit.c -o exit
vincent@ubuntu:~/ch05/5.2$ ./exit
abcdefroutine2 is called.
routine1 is called.
vincent@ubuntu:~/ch05/5.2$ gcc exit.c -o exit -D_EXIT
vincent@ubuntu:~/ch05/5.2$ ./exit
vincent@ubuntu:~/ch05/5.2$
通过以上操作可见,如果编译时不加-D_EXIT,那么程序将会执行exit(0),那么字符串abcdef和两个退出处理函数(所谓的“退出处理函数”指的是进程使用exit( )退出时被自动执行的函数,需要使用atexit( )来注册)都被相应地处理了。而如果编译时加了-D_EXIT的话,那么程序将执行_exit(0),从执行结果看,缓冲区数据没有被冲洗,退出处理函数也没有被执行。
这两个函数的参数status是该进程的退出值,进程退出后状态切换为EXIT_ZOMBIE,相应地,这个值将会被放在该进程的“尸体”(PCB)里面,等待父进程回收。在进程异常退出时,有时需要向父进程汇报异常情况,此时就用非零值来代表特定的异常情况,比如1代表权限不足、2代表内存不够等,具体情况只要父子进程商定好就可以了。
接下来,父进程如果需要,可以使用wait( )/waitpid( )来获得子进程正常退出的退出值,当然,这两个函数还可以使得父进程阻塞等待子进程的退出,以及将子进程状态切换为EXIT_DEAD,以便于系统释放子进程资源。表5-5所示是这两个函数的接口。
表5-4 函数wait()和waitpid()的接口规范
续表
注意,所谓的退出状态不是退出值,退出状态包括了退出值。如果使用以上两个函数成功获取了子进程的退出状态,则可以使用以下宏来进一步解析,如表5-5所示。
表5-5 处理子进程退出状态值的宏
注:
① 正常退出指的是调用exit( )/_exit( ),或者在主函数中调用return,或者在后一个线程调用pthread_exit( )。
② 由于没有在POSXI.1—2001标准中定义,这个选项在某些UNIX系统中无效,比如AIX或者sunOS中。
以下示例代码,综合展示了如何正确使用fork( )/exec( )函数簇、exit( )/_exit( )和wait( )/waitpid( )。程序的功能是:父进程产生一个子进程让它去程序child_elf,并且等待它的退出(可以用wait( )阻塞等待,也可以用waitpid( )非阻塞等待),子进程退出(可以正常退出,也可以异常退出)后,父进程获取子进程的退出状态后打印出来。详细代码如下。
vincent@ubuntu:~/ch05/5.2$ cat child_elf.c -n
1 #include
2 #include
3
4 int main(void)
5 {
6 printf(“[%d]: yep, I am the childn”, (int)getpid());
7
8 #ifdef ABORT
9 abort(); //自己给自己发送一个致命信号SIGABRT,自杀
10 #else
11 exit(7); //正常退出,且退出值为7
12 #endif
13 }
vincent@ubuntu:~/ch05/5.2$ cat wait.c -n
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8
9 #include
10 #include
11 #include
12
13 int main(int argc, char **argv)
14 {
15 pid_t x = fork();
16
17 if(x == 0) //子进程,执行指定程序child_elf
18 {
19 execl(“./child_elf”, “child_elf”, NULL);
20 }
21
22 if(x > 0) //父进程,使用wait( )阻塞等待子进程的退出
23 {
24 int status;
25 wait(&status);
26
27 if(WIFEXITED(status)) //判断子进程是否正常退出
28 {
29 printf(“child exit normally, “
30 “exit value: %hhun”, WEXITSTATUS(status));
31 }
32
33 if(WIFSIGNALED(status)) //判断子进程是否被信号杀死
34 {
35 printf(“child killed by signal: %un”,
36 WTERMSIG(status));
37 }
38 }
39
40 return 0;
41 }
执行效果如下:
vincent@ubuntu:~/ch05/5.2$ gcc child_elf.c -o child_elf
vincent@ubuntu:~/ch05/5.2$ ./wait
[26259]: yep, I am the child
child exit normally, exit value: 7
vincent@ubuntu:~/ch05/5.2$ gcc child_elf.c -o child_elf -DABORT
vincent@ubuntu:~/ch05/5.2$ ./wait
[26266]: yep, I am the child
child killed by signal: 6
vincent@ubuntu:~/ch05/5.2$
可以看到,子进程不同的退出情形,父进程的确可以通过wait( )/waitpid( )和一些相应的宏来获取,这是协调父子进程工作的一个重要途径。
至此,我们已经知道如何创建多进程,以及掌握了它们的基本操作方法了,有一点是必须再提醒一次的:进程它们是相互独立的,重要体现在它们互不干扰的内存空间上,它们的数据是不共享的,但如果多个进程需要协同合作,就必然会有数据共享的需求,就像人与人之间需要说话一样,进程需要通过某样东西来互相传递信息和数据,这就是所谓的IPC(Inter-Process Comunication)机制,IPC有很多种,它们是如何使用的?有哪些特点?在什么场合适用?请看5.3节。
评论
还没有评论。