新書推薦:
《
中国官僚政治研究(一部洞悉中国政治制度演变的经典之作)
》
售價:HK$
62.7
《
锂电储能产品设计及案例详解
》
售價:HK$
110.9
《
首辅养成手册(全三册)(张晚意、任敏主演古装剧《锦绣安宁》原著小说)
》
售價:HK$
121.0
《
清洁
》
售價:HK$
65.0
《
组队:超级个体时代的协作方式
》
售價:HK$
77.3
《
第十三位陪审员
》
售價:HK$
53.8
《
微观经济学(第三版)【2024诺贝尔经济学奖获奖者作品】
》
售價:HK$
155.7
《
Python贝叶斯深度学习
》
售價:HK$
89.4
|
編輯推薦: |
图文结合视频,全面精讲Linux编程关键知识点;
只要你看,就能懂能会能用。
|
內容簡介: |
本书从零开始,循序渐进地攻破Linux环境编程所遇到的各级关卡,以图文并茂的形式帮助读者理解各个概念。本书内容翔实,囊括了Linux系统操作细节,Shell脚本编程精要,各种编程环境所需要解决的技术难点,以及在Linux环境下的C语言编程技术、并发编程技术和音视频编程等核心内容。全书用近400幅图表帮助读者理解复杂概念,因此读者不需要具备任何计算机编程经验,在本书的指导下就能进入编程的世界,并能在阅读和实践中享受编程的乐趣。同时,本书配套完整的视频教程,给读者以最直观、最容易吸收知识的方式,融会贯通书中所有的知识点。不仅如此,读者还能够得到作者及其团队的在线技术支援和答疑。
|
關於作者: |
2007年毕业于北京信息科技大学,本科自动化专业;2007-2010年从事一线软件开发工作,服务于北京赞同科技、北京尚观科技等公司;2010年到深圳从事相关IT培训教学工作,服务于华清远见、广州粤嵌等公司。
|
目錄:
|
目 录
第1章 Linux编程环境1
1.1 基本工具1
1.1.1 免费大餐:Ubuntu1
1.1.2 桌面系统:gnome6
1.1.3 网络配置:纯手工打造6
1.1.4 软件集散地:APT8
1.1.5 无敌板斧:vi10
1.1.6 开发圣典:man13
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 编译器:GCC55
1.4.1 简述55
1.4.2 编译过程简介55
1.4.3 实用的编译选项58
1.5 解剖Makefile59
1.5.1 工程管理器make59
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-autotools86
1.6.1 autotools简介86
1.6.2 文件组织87
1.6.3 configure.ac编写规则88
第2章 深度Linux-C92
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 玩转BST247
3.3.3 各种的遍历算法260
3.3.4 自平衡AVL树263
3.3.5 自平衡Linux红黑树273
第4章 IO编程技术289
4.1 一切皆文件289
4.1.1 文件的概念289
4.1.2 各类文件290
4.2 文件操作290
4.2.1 系统IO291
4.2.2 标准IO306
4.2.3 文件属性320
4.3 目录检索327
4.3.1 基本概念327
4.3.2 相关API328
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 相关重要API350
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 标准音频接口ALSA436
6.3 Linux视频输出450
6.3.1 基本概念450
6.3.2 framebuffer452
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 多媒体开发库SDL489
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 音视频编解码库FFmpeg504
6.6.1 FFmpeg简介504
6.6.2 核心结构体与常用API505
6.6.3 与SDL结合实现简单的播放器 511
|
內容試閱:
|
第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.7includeLinux$ 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 sbininit
root 2 0 0 Jul22 ? 00:00:00 [kthreadd]
root 3 2 0 Jul22 ? 00:00:06 [ksoftirqd0]
root 6 2 0 Jul22 ? 00:00:01 [migration0]
root 7 2 0 Jul22 ? 00:00:01 [watchdog0]
root 8 2 0 Jul22 ? 00:00:00 [migration1]
root 10 2 0 Jul22 ? 00:00:05 [ksoftirqd1]
root 11 2 0 Jul22 ? 00:00:02 [watchdog1]
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 的接口规范
功能
创建一个新的进程
头文件
#include unistd.h
原型
pid_t forkvoid;
返回值
成功
0或者大于0的正整数
失败
-1
备注
该函数执行成功之后,将会产生一个新的子进程,在新的子进程中其返回值为0,在原来的父进程中其返回值为大于0的正整数,该正整数就是子进程的PID
这个函数接口本身非常简单,简单到连参数都没有,但是这个函数有个与众不同的地方:它会使得进程一分为二!就像细胞分裂一样,如图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 整个内存空间,包括栈、堆、数据段、代码段、标准IO的缓冲区等。
而以下属性,父子进程是不一样的。
l 进程号PID。PID是身份证号码,哪怕亲如父子,也要区分开。
l 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
l 挂起的信号。这些信号是所谓的悬而未决的信号,等待着进程的响应,子进程也不会继承这些信号。
(2)子进程会从fork 返回值后的下一条逻辑语句开始运行。这样就避免了不断调用fork 而产生无限子孙的悖论。
(3)父子进程是相互平等的。它的执行次序是随机的,或者说它们是并发运行的,除非使用特殊机制来同步它们,否则不能判断它们的运行究竟谁先谁后。
(4)父子进程是相互独立的。由于子进程完整地复制了父进程的内存空间,因此从内存空间的角度看它们是相互独立、互不影响的。
以下代码显示了fork 的作用。
vincent@ubuntu:~ch055.2$ cat fork.c -n
1 #include stdio.h
2 #include unistd.h
3
4 int mainvoid
5 {
6 printf"[%d]: before fork ... \n", intgetpid;
7
8 pid_t x;
9 x = fork; 生个孩子
10
11 printf"[%d]: after fork ...\n", intgetpid;
12 return 0;
13 }
执行效果如下。
vincent@ubuntu:~ch055.2$ .fork
[23900]: before fork ...
[23900]: after fork ...
vincent@ubuntu:~ch055.2$ [23901]: after fork ...
可以看到,第11行代码被执行了两遍,函数getpid 展示了当前进程的PID,其中23900是父进程,23901是子进程。从执行效果看还有一个很有意思的现象:子进程打印的信息被挤到Shell命令提示符(vincent@ubuntu:~ch055.2$)之后!造成这个结果的原因是:Shell命令提示符默认会在父进程退出之后立即显示出来,而父进程退出之时,子进程还没来得及执行完第11行。
由于父子进程的并发性,以上程序的执行效果是不一定的,换句话说,们如果再执行一遍代码可能会得到这样的效果:
vincent@ubuntu:~ch055.2$ .fork
[23900]: before fork ...
[23901]: after fork ...
[23900]: after fork ...
vincent@ubuntu:~ch055.2$
接下来一个脱口而出的疑问是:好不容易生了个孩子,但是干的事情跟父进程是一样的,那我们要这个孩子有何用呢?答案是:上述代码确实没有什么实际意义,事实上我们一般会让孩子去执行一个预先准备好的ELF文件或脚本,用以覆盖从父进程复制过来的代码,下面先介绍这个加载ELF文件或脚本的接口函数,如表5-2所示。
表5-2 函数族exec 的接口规范
功能
在进程中加载新的程序文件或脚本,覆盖原有代码,重新运行
头文件
#include unistd.h
原型
int execlconst char *path, const char *arg, ...;
int execvconst char *path, char *const argv[ ];
int execleconst char *path, const char *arg, ..., char * const envp[ ];
int execlpconst char *file, const char *arg, ...;
int execvpconst char *file, char *const argv[ ];
int execvpeconst char *file, char *const argv[ ],char *const envp[ ];
参数
path
即将被加载执行的ELF文件或脚本的路径
file
即将被加载执行的ELF文件或脚本的名字
arg
以列表方式罗列的ELF文件或脚本的参数
argv
以数组方式组织的ELF文件或脚本的参数
envp
用户自定义的环境变量数组
返回值
成功
不返回
失败
-1
备注
(1)函数名带字母l意味着其参数以列表(list)的方式提供。
(2)函数名带字母v意味着其参数以矢量(vector)数组的方式提供。
(3)函数名带字母p意味着会利用环境变量PATH来找寻指定的执行文件。
(4)函数名带字母e意味着用户提供自定义的环境变量
上述代码组成一个所谓的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:~ch055.2$ cat child_elf.c -n
1 #include stdio.h
2 #include stdlib.h
3
4 int mainvoid
5 {
6 printf"[%d]: yep, I am the child\n", intgetpid;
7 exit0;
8 }
下面是使用exec函数簇中的execl来让子进程加载上述代码的示例。
vincent@ubuntu:~ch055.2$ cat exec.c -n
1 #include stdio.h
2 #include stdlib.h
3 #include unistd.h
4
5 int mainint argc, char **argv
6 {
7 pid_t x;
8 x = fork;
9
10 ifx 0 父进程
11 {
12 printf"[%d]: I am the parent\n", intgetpid;
13 exit0;
14 }
15
16 ifx == 0 子进程
17 {
18 printf"[%d]: I am the child\n", intgetpid;
19 execl".child_elf", "child_elf", NULL; 执行child_elf程序
20
21 printf"NEVER be printed\n"; 这是一条将被覆盖的代码
22 }
23
24 return 0;
25 }
下面是执行结果:
vincent@ubuntu:~ch055.2$ .exec
[24585]: I am the parent
vincent@ubuntu:~ch055.2$ [24586]: I am the child
[24586]: yep, I am the child
从以上执行结果看到,父进程比其子进程先执行完代码并退出,因此Shell命令提示行又被夹在中间了,那么怎么让子进程先运行并退出之后,父进程再继续呢?子进程的退出状态又怎么传递给父进程呢?答案是:可以使用exit _exit 来退出并传递退出值,使用wait waitpid 来使父进程阻塞等待子进程,顺便还可以帮子进程收尸,这几个函数的接口如表5-3所示。
表5-3 函数exit和_exit的接口规范
功能
退出本进程
头文件
#include unistd.h
#include stdlib.h
原型
void _exitint status;
void exitint status;
参数
status
子进程的退出值
返回值
不返回
备注
(1)如果子进程正常退出,则status一般为0。
(2)如果子进程异常退出,则statuc一般为非0。
(3)exit 退出时,会自动冲洗(flush)标准IO总残留的数据到内核,如果进程注册了退出处理函数还会自动执行这些函数。而_exit 会直接退出
以下代码展示了exit 和_exit 的用法和区别。
vincent@ubuntu:~ch055.2$ cat exit.c -n
1 #include stdio.h
2 #include stdlib.h
3 #include unistd.h
4
5 void routine1void 退出处理函数
6 {
7 printf"routine1 is called.\n";
8 }
9
10 void routine2void 退出处理函数
11 {
12 printf"routine2 is called.\n";
13 }
14
15 int mainint argc, char **argv
16 {
17 atexitroutine1; 注册退出处理函数
18 atexitroutine2;
19
20 fprintfstdout, "abcdef"; 将数据输送至标准IO缓冲区
21
22 #ifdef _EXIT
23 _exit0; 直接退出
24 #else
25 exit0; 冲洗缓冲区数据,并执行退出处理函数
26 #endif
27 }
vincent@ubuntu:~ch055.2$ gcc exit.c -o exit
vincent@ubuntu:~ch055.2$ .exit
abcdefroutine2 is called.
routine1 is called.
vincent@ubuntu:~ch055.2$ gcc exit.c -o exit -D_EXIT
vincent@ubuntu:~ch055.2$ .exit
vincent@ubuntu:~ch055.2$
通过以上操作可见,如果编译时不加-D_EXIT,那么程序将会执行exit0,那么字符串abcdef和两个退出处理函数(所谓的退出处理函数指的是进程使用exit 退出时被自动执行的函数,需要使用atexit 来注册)都被相应地处理了。而如果编译时加了-D_EXIT的话,那么程序将执行_exit0,从执行结果看,缓冲区数据没有被冲洗,退出处理函数也没有被执行。
这两个函数的参数status是该进程的退出值,进程退出后状态切换为EXIT_ZOMBIE,相应地,这个值将会被放在该进程的尸体(PCB)里面,等待父进程回收。在进程异常退出时,有时需要向父进程汇报异常情况,此时就用非零值来代表特定的异常情况,比如1代表权限不足、2代表内存不够等,具体情况只要父子进程商定好就可以了。
接下来,父进程如果需要,可以使用wait waitpid 来获得子进程正常退出的退出值,当然,这两个函数还可以使得父进程阻塞等待子进程的退出,以及将子进程状态切换为EXIT_DEAD,以便于系统释放子进程资源。表5-5所示是这两个函数的接口。
表5-4 函数wait和waitpid的接口规范
功能
等待子进程
头文件
#include syswait.h
原型
pid_t waitint *stat_loc;
pid_t waitpidpid_t pid, int *stat_loc, int options;
参数
pid
小于-1:等待组ID的绝对值为pid的进程组中的任意一个子进程
-1:等待任意一个子进程
0:等待调用者所在进程组中的任意一个子进程
大于0:等待进程组ID为pid的子进程
续表
功能
等待子进程
参数
stat_loc
子进程退出状态
option
WCONTINUED:报告任意一个从暂停态出来且从未报告过的子进程的状态
WNOHANG:非阻塞等待
WUNTRACED:报告任意一个当前处于暂停态且从未报告过的子进程的状态
返回值
wait
成功:退出的子进程PID
失败:-1
waitpid
成功:状态发生改变的子进程PID(如果WNOHANG被设置,且由pid指定的进程存在但状态尚未发生改变,则返回0)
失败:-1
备注
如果不需要获取子进程的退出状态,stat_loc可以设置为NULL
注意,所谓的退出状态不是退出值,退出状态包括了退出值。如果使用以上两个函数成功获取了子进程的退出状态,则可以使用以下宏来进一步解析,如表5-5所示。
表5-5 处理子进程退出状态值的宏
宏
含 义
WIFEXITEDstatus①
如果子进程正常退出,则该宏为真
WEXITSTATUSstatus
如果子进程正常退出,则该宏将获取子进程的退出值
WIFSIGNALEDstatus
如果子进程被信号杀死,则该宏为真
WTERMSIGstatus
如果子进程被信号杀死,则该宏将获取导致它死亡的信号值
WCOREDUMPstatus②
如果子进程被信号杀死且生成核心转储文件(core dump),则该宏为真
WIFSTOPPEDstatus
如果子进程的被信号暂停,且option中WUNTRACED已经被设置时,则该宏为真
WSTOPSIGstatus
如果WIFSTOPPEDstatus为真,则该宏将获取导致子进程暂停的信号值
WIFCONTINUEDstatus
如果子进程被信号SIGCONT重新置为就绪态,则该宏为真
注:
① 正常退出指的是调用exit _exit ,或者在主函数中调用return,或者在最后一个线程调用pthread_exit 。
② 由于没有在POSXI.12001标准中定义,这个选项在某些UNIX系统中无效,比如AIX或者sunOS中。
以下示例代码,综合展示了如何正确使用fork exec 函数簇、exit _exit 和wait waitpid 。程序的功能是:父进程产生一个子进程让它去程序child_elf,并且等待它的退出(可以用wait 阻塞等待,也可以用waitpid 非阻塞等待),子进程退出(可以正常退出,也可以异常退出)后,父进程获取子进程的退出状态后打印出来。详细代码如下。
vincent@ubuntu:~ch055.2$ cat child_elf.c -n
1 #include stdio.h
2 #include stdlib.h
3
4 int mainvoid
5 {
6 printf"[%d]: yep, I am the child\n", intgetpid;
7
8 #ifdef ABORT
9 abort; 自己给自己发送一个致命信号SIGABRT,自杀
10 #else
11 exit7; 正常退出,且退出值为7
12 #endif
13 }
vincent@ubuntu:~ch055.2$ cat wait.c -n
1 #include stdio.h
2 #include stdlib.h
3 #include stdbool.h
4 #include unistd.h
5 #include string.h
6 #include strings.h
7 #include errno.h
8
9 #include sysstat.h
10 #include systypes.h
11 #include fcntl.h
12
13 int mainint argc, char **argv
14 {
15 pid_t x = fork;
16
17 ifx == 0 子进程,执行指定程序child_elf
18 {
19 execl".child_elf", "child_elf", NULL;
20 }
21
22 ifx 0 父进程,使用wait 阻塞等待子进程的退出
23 {
24 int status;
25 waitstatus;
26
27 ifWIFEXITEDstatus 判断子进程是否正常退出
28 {
29 printf"child exit normally, "
30 "exit value: %hhu\n", WEXITSTATUSstatus;
31 }
32
33 ifWIFSIGNALEDstatus 判断子进程是否被信号杀死
34 {
35 printf"child killed by signal: %u\n",
36 WTERMSIGstatus;
37 }
38 }
39
40 return 0;
41 }
执行效果如下:
vincent@ubuntu:~ch055.2$ gcc child_elf.c -o child_elf
vincent@ubuntu:~ch055.2$ .wait
[26259]: yep, I am the child
child exit normally, exit value: 7
vincent@ubuntu:~ch055.2$ gcc child_elf.c -o child_elf -DABORT
vincent@ubuntu:~ch055.2$ .wait
[26266]: yep, I am the child
child killed by signal: 6
vincent@ubuntu:~ch055.2$
可以看到,子进程不同的退出情形,父进程的确可以通过wait waitpid 和一些相应的宏来获取,这是协调父子进程工作的一个重要途径。
至此,我们已经知道如何创建多进程,以及掌握了它们的基本操作方法了,有一点是必须再提醒一次的:进程它们是相互独立的,最重要体现在它们互不干扰的内存空间上,它们的数据是不共享的,但如果多个进程需要协同合作,就必然会有数据共享的需求,就像人与人之间需要说话一样,进程需要通过某样东西来互相传递信息和数据,这就是所谓的IPC(Inter-Process Comunication)机制,IPC有很多种,它们是如何使用的?有哪些特点?在什么场合适用?请看5.3节。
|
|