引言
Linux简介:
Linux是由芬兰的赫尔辛基(Helsinki)大学学生Linus Torvalds把Minix 系统向x86移植的结果。当时 Linus 手边有个 Minix 系统(UNIX 的一个分支),他对这个操作系统相当有兴趣,由于当时他正好有一台个人计算机,他想把这个系统移植到该计算机(x86 架构)上来使用。由于受益于Stallman提倡的开放源代码(Open Source)思想,他得以接触到UNIX操作系统的一些源代码,并仔细研读了UNIX 的核心,然后去除较为繁复的核心程序,将它改写成能够适用于一般个人计算机的一种操作系统,即Linux系统的雏形。
1992年1月,大概只有100人开始使用Linux,但他们为Linux的发展壮大作出了巨大贡献。他们对一些不合理的代码进行了改进,修补了代码错误并上传补丁。Linux的腾飞最关键的因素是获得了自由软件基金(FSF)的支持,他们制定了一个GNU计划,该计划的目标就是要编写一个完全免费的 UNIX版本——包括内核及所有相关的组件,可以让用户自由共享并且改写软件,而Linux正好符合他们的意愿。他们将Linux与其现有的GNU应用软件很好地结合起来,使Linux拥有了图形用户界面。
Linux是能够自由传播并继承了UNIX内核的操作系统,是对UNIX的简化和改进,它既保留了UNIX系统的高安全性,同时也使其操作更加简单方便,从而使单机用户也可以使用。
Linux操作系统在短短的几年之内得到了非常迅猛的发展,这与Linux具有的良好特性是分不开的。Linux几乎包含了UNIX的全部功能和特性,同时又有自己的一些特点。概括地讲,Linux具有以下主要特性: ● 开放性
开放性是指系统遵循世界标准规范,特别是遵循开放系统互联(OSI)国际标准。凡遵循国际标准所开发的硬件和软件,都能彼此兼容,可方便地实现互联。 ● 多用户
多用户是指系统资源可以被不同用户各自拥有和使用,即每个用户对自己的资源(例如:文件、设备)有特定的权限,互不影响。Linux继承了UNIX的多用户特性。 ● 多任务
多任务是现代计算机的最主要的一个特点。它是指计算机同时执行多个程
序,而且各个程序的运行互相独立。Linux系统调度每一个进程,平等地访问微处理器。由于CPU的处理速度非常快,其结果是,启动的应用程序看起来好像在并行运行。事实上,从处理器执行一个应用程序中的一组指令到Linux调度微处理器再次运行这个程序之间只有很短的时间延迟,用户是感觉不出来的。 ● 良好的用户界面
Linux向用户提供了3种界面:传统操作界面、系统调用界面和图形用户界面。Linux的传统操作界面是基于文本的命令行界面,即Shell,它既可以联机使用,又可在文件上脱机使用。Shell有很强的程序设计能力,用户可方便地用它编制程序,从而为用户扩充系统功能提供了更高级的手段。可编程Shell是指将多条命令组合在一起,形成一个Shell程序,这个程序可以单独运行,也可以与其他程序同时运行。
系统调用界面是为用户提供编程时使用的界面。用户可以在编程时直接使用系统提供的系统调用命令。系统通过这个界面为用户程序提供低级、高效率的服务。
Linux还为用户提供了图形用户界面。它利用鼠标、菜单、窗口、滚动条等设施,给用户呈现一个直观、易操作、交互性强的友好的图形化界面。 ● 设备独立性
Linux是具有设备独立性的操作系统,它的内核具有高度的适应能力。随着越来越多的程序员开发Linux系统,将会有更多的硬件设备加入到各种Linux内核和发行版本中。另外,由于用户可以免费得到Linux的内核源代码,因此,用户可以根据需要修改内核源代码,以便适应新增加的外部设备。
设备独立性是指操作系统把所有外部设备统一当作文件来看待,只要安装它们的驱动程序,任何用户都可以像使用文件一样,操纵、使用这些设备,而不必知道它们的具体存在形式。
具有设备独立性的操作系统,通过把每一个外围设备看作一个独立文件来简化增加新设备的工作。当需要增加新设备时,系统管理员就在内核中增加必要的连接。这种连接(也称作设备驱动程序)能保证每次调用设备提供的服务时,内核能以相同的方式来处理它们。当新的或更好的外设被开发并交付给用户时,系统允许在这些设备连接到内核后,能不受限制地立即访问它们。设备独立性的关键在于内核的适应能力。其他操作系统只允许一定数量或一定种类的外部设备连接。而设备独立性的操作系统却能够容纳任意种类及任意数量的设备,因为每一个设备都是通过其与内核的专用连接进行独立访问的。 ● 提供了丰富的网络功能
完善的内置网络是Linux的一大特点。Linux在通信和网络功能方面优于其
他操作系统。其他操作系统不包含如此紧密地和内核结合在一起的连接网络的能力,也没有内置这些联网特性的灵活性。而Linux为用户提供了完善的、强大的网络功能。
支持Internet是其网络功能之一。Linux免费提供了大量支持Internet的软件,通过Internet,用户能用Linux与世界上各个地区的人方便地通信。它内建了http、ftp、dns等功能,支持所有常见的网络服务,包括ftp、telnet、NFS、TCP、IP等,加上超强的稳定性,因此很多ISP(Internet Service Providers)都是采用Linux来架设邮件服务器、FTP服务器及Web 服务器等各种服务器的。Linux在最新发展的内核中还包含了一些通用的网络协议,比如IPv4、IPv6、AX.25、X.25、IPX、DDP(Appletalk)、NetBEUI、Netrom 等。用户能通过一些Linux命令完成内部信息或文件的传输。 Linux不仅允许进行文件和程序的传输,它还为系统管理员和技术人员提供了访问其他系统的接口。
另外,还可以进行远程访问。通过这种远程访问的功能,一位技术人员能够有效地为多个系统服务,即使那些系统位于相距很远的地方。稳定的核心中目前包含的网络协议有TCP、IPv4、IPX、DDP、AX等。另外还提供Netware的客户机和服务器,以及现在最热门的Samba(让用户共享Mircosoft Network资源)。 ● 可靠的系统安全
Linux采取了许多安全技术措施,包括对读/写进行权限控制、带保护的子系统、审计跟踪、核心授权等,这为网络多用户环境中的用户提供了必要的安全保障。
● 良好的可移植性
可移植性是指将操作系统从一个平台转移到另一个平台上,并使它仍然能按其自身的方式运行的能力。
Linux是一种可移植的操作系统,能够在从微型计算机到大型计算机的任何环境中运行。可移植性为运行Linux的不同计算机平台与其他任何计算机进行准确而有效的通信提供了手段,不需要另外增加特殊的和昂贵的通信接口。
上述特点,使得Linux操作系统在服务器领域的应用和普及已经有相当的一段时间。而近年来,Linux操作系统在嵌入式系统领域的延伸也可谓是如日中天,许多版本的嵌入式Linux系统被开发出来,如ucLinux、RTLinux、ARM-Linux等等。在嵌入式操作系统方面,Linux的地位是不容怀疑的,它开源、它包含TCP/IP协议栈、它易集成GUI。鉴于Linux操作系统在服务器和嵌入式系统领域愈来愈广泛的应用,社会上越来越需要基于Linux操作系统进行编程的开发人员。
Linux应用程序(软件),硬件与内核之间的关系:
简单的举个例子,如果我们把‘人’比做计算机的话,那么‘人’的身体四肢,心肝脾胃肾,眼睛耳朵等等就是‘人’这台计算机的硬件,而‘人’的软件(应用程序)呢?就是‘人’的“思想”,作为人来说,人们都是通过“思想”来控制‘人’的行为,操控‘人’的硬件(身体)来完成一切行为活动的!这是人类思想的作用,也是计算机应用程序的作用。好,我们知道了人类的思想就是‘人’这台计算机的软件,由此,问题也来了:思想是怎样形成的?以及‘人’怎样表述思想,怎样让其他‘人’(计算机)了解并清楚自己思想,也就是所谓的交流。或者说这种思想如何形成并怎样表述出来,让其他人明白自己所要完成的工作呢?很简单,通过语言——通过对语言的翻译转换。
人类有很多语言,中文,英文,意大利文,德文等等,这类似于计算机的多种语言:C语言,C++语言,c# , JAVA 等等,人类通过语言代码的指令来完成工作,计算机也一样。
比如:人类语言代码:吃饭。
通过这句命令代码,‘人’会控制自己身体的硬件来完成一系列的动作,如:
拿起碗筷,将适合的食物填充入口,咀嚼,下咽 „„。完成这一系列固定的‘人’的硬件动作。其实它就相当于计算机完成一套固定的应用程序。
对于‘人’来说,我们有一个抽象的概念,就是‘心’,我们都说是由‘心’来翻译语言,把语言转化成‘思想’,然后交给CPU(大脑),我们的CPU(大脑)会按照‘心’翻译好的思想意识来控制身体完成目标动作。
对于计算机来讲,却没有了这个‘心’的抽象概念,计算机的‘心’是真实存在的,那就是计算机的内核,计算机的内核负责将计算机语言代码转换成计算机的思想意识,从而控制计算机硬件完成工作。
作为程序员,其任务就是编写这样的语言代码。本书就是教给你如何编写Linux下应用程序,编辑Linux下的应用成应该具备的基础!
学习本书应具备以下基础: 1. 2.
具备基础的Linux系统知识及相关操作; 具备C语言应用开发基础。
本书共分为6章,主要内容为:Linux环境下C语言程序的设
过程,编译过程,调式器GDB的使用,系统调用与API,文件操作,串行通信程序的实现。进程的产生与控制,进程间的通信,多线程编程。
第一章 Linux系统程序设计基础
1.1 第一个Linux C程序
在本节中,我们先以一个简单的Linux C程序开头,说明在linux环境下C语言程序设计的基本步骤。
列:1.1 设计一个程序,要求在屏幕上输出“hello world!”
分析:次程序的主函数体只有一个输出语句,printf 是C语言中的输出函数,双引号中的内容将被原样输出,‘\\n’是换行符。
操作步骤
步骤1:编写程序源代码
Linux下做常用的文本编辑器是vim,在屏幕终端中输出如下:
[root@localhost root]# vim 1-1.c
得到文本编辑器工作界面,接着按 i(a / o) 健,进入编辑模式输入如下代码:
#include int main(){ printf (“hello world!\\n”); /* c程序的内容,输出打印hello world*/ } return 0; 为了便于他人或自己以后阅读,从最简单的程序开始,养成注释的好习惯。其中,注释内容应在“/*”与“*/”之间,凡是在此之间的文字(或其它),编译器均会忽略,不予编译。 #include 是指定程序中用到的系统函数包含的文件库,“stdio.h”是标准输入输出库。Main表示主函数,每个c语言都必须有一个主函数,主函数体(主函数内容)使用“{}”(大括号)括起来,每条语句使用“;(分号)”表明结束。 输入完程序后的界面如图1.1所示: 步骤2:编译程序 编译程序之前,最好先确定程序的原文件存在,可是使用Linux“ls”命令查看当前目录下是否有1-1.c的原文件。 [root@localhost root]# ls 1-1.c 接着使用编译命令编译此原文件,将其编译成可执行文件,若编译时没有出错信息,说明程序编译成功! [root@localhost root]#gcc –o 1-1 1-1.c [root@localhost root]# 步骤3:运行程序 编译成功以后,可执行程序在你的系统中将是绿色的可执行文件1-1,同样可以使用“ls”命令查看文件是否存在,如果存在,输入“./1-1”,执行程序,后,会在屏幕终端看见程序执行结果! [root@localhost root]# ls 1-1.c 1-1 [root@localhost root]# ./1-1 hello world! [root@localhost root]# 由此可见,一个Linux应用程序在Linux环境下的C程序设计,主要用到的工具是文件编辑器和编译工具(编译软件)Linux下的文本编辑器主要有vim , gedit和Emacs。而编译器中,属 GCC功能最为强大,使用最广的软件。 练习:设计一个程序,要求在屏幕上输出如下结果: **** * * * * **** 最后我们复习一些Linux系统与C语言的知识。 C语言简要说明: 1>. C语言最早是美国贝尔实验室为UNIX的辅助开发而编写的! 2>. 规范的C,1987年美国国家标准协会ANSI对不同版本的C进行了扩展和补充,并制定了一个标准,称为:ANSI C 3>. C语言属于中级语言,动作和数据分离,功能齐全强大,可移植性强。 Linux下的编辑工具 在Linux下,最常用的编辑器就是Vi(Vim),它功能强大,方便使用。 Vi有三种模式,分别为:命令行模式,插入模式和底行模式; 当用户最初进入Vi 时的模式为命令行模式,这种模式下,可以通过上下移 动进行“删除字符”或“整行删除”等操作,也可以进行“复制”,“粘贴”等操作,但无法编辑文字。 当按下字母“ i”时,可进入插入模式,在此模式下,用户可以编辑文字,按『ESC』键返回命令行模式 在编辑模式下输入“:”可进入底行模式,这种模式下,用户可以进行保存,退出,设置编辑环境,寻找字符,列行号等属性操作命令。 命令行模式常见功能 目录 I A O 『CTRL』+『b』 『CTRL』+『f』 『CTRL』+『u』 『CTRL』+『d』 (数字)0 G nG /name ?name X dd yy U 目录内容 切换到插入模式,此时光标位于开始输入文件处 切换到插入模式,并且从目前光标所在的位置的 下一个位置开始 切换到插入模式,且从行首开始插入新的一行 屏幕向后翻动一页 屏幕向前翻动一页 屏幕向后翻动半页 屏幕向前翻动半页 光标移动到本行开头 光标移动到 文章做最后 光标移动到第n行 在 光标之后查找一个名为“name”的字符串 在 光标之前查找一个名为“name”的字符串 删除光标所在位置的后面的 一个字符 删除光标所在行 复制光标所在行 恢复前一个动作 底行行模式常见功能 目录 :w :q :q! :wq :w[filename] :set nu :set nonu 目录内容 将编辑的文件保存在磁盘中 退出Vi(做过修改的 文件会给出提示) 强退,没有提示 保存退出 另存为filename的文件 显示行号 取消行号 插入模式的功能键只有一个,就是[ESC]退出到命令行模式 1.2 gcc 编 译 器 在Linux编译工具中,gcc是功能最强大,使用最广泛的软件 gcc是GNU Compiler Collection 的简称,它是GNU项目中符合ANSI C标准的编译器,能够编译C,C++和Object C等语言,并且,GCC可以通过不同的前端模块来支持各种语言,如JAVA,Fortran,pascal等。 gcc是可以在多种硬件平台上编译可执行程序的超级编译器,其执行效率与一般编译器相比,平均效率要高20%~30%。gcc支持编译的一些原文件的后缀见表1.2: 后缀名 .c .C .cc .cxx .m .i 对应的语言 后缀名 C原始程序 .ii C++原始程序 .s C++原始程序 .S C++原始程序 .h Objective –C 原始程序 .o 已经预处理的C原始程序 .a/.so 对应的语言 已经预处理过的C++原始程序 汇编语言原始程序 汇编语言原始程序 预处理文件(头文件) 目标文件 编译后的库文件 表1.2 gcc可直接支持编译的后缀名 如何使用GCC: gcc常用编译格式如下: gcc [参数] 要编译的文件 [参数] [目标文件] 其最常用的编辑格式有三种: gcc C源程序 –o 目标文件名 gcc –o 目标文件名 C源程序 gcc 源程序文件 (使用默认的目标文件名: a.out) 从本章的第一个Linux程序可以看出,目标文件可以省略,gcc默认生成可执行的文件为a.out,如果想要生成自己命名的可执行文件,通常需要使用“- o”参数。 下面看一个例子: 列1.2 :设计一个程序,要求使用公式℃=(5/9)(℉-32)打印华氏温度与摄氏温度对照表: 操作步骤 步骤1:编写源程序代码 [root@localhost root]# vim 1-2.c 程序代码如下: /*程序1-2.c:华氏温度与摄氏温度对照表*/ #include int lower , upper , step ; lower = 0 ; /* 温度表的下限 */ upper = 300 ; /* 温度表的上限 */ step = 20 ; /* 步长 */ fahr = lower ; while (fahr <= upper) { celsius = (5.0 / 9.0) *(fahr – 32.0); } printf(“%3.0f %6.1f\\n”, fahr, celsius); fahr = fahr + step ; 步骤2 用gcc编译程序 [root@localhost root]# gcc –o 1-2 1-2.c 或者 [root@localhost root]# gcc 1-2.c –o 1-2 如果编译成功,两者的结果是一样的,都会生成可执行文件1-2。 如果使用默认编译 即:gcc 1-2.c 则会得到结果 a.out 。 步骤3 运行程序 编译成功后,执行可执行文件1-2 ,执行可执行文件,输入如下: [root@localhost root]# ./1-2 此时系统输出如下结果:如图1.3所示: 1.2.1 gcc 的编译流程 开放,自由与灵活是Linux的魅力所在,而在这一点上gcc体现的淋漓尽致,gcc可以让软件工程师完全控制整个编译流程! 源代码(*.c) 在使用gcc编译时,具体过程如图1.4所示,下面通过实例来具体看一下gcc是如何完成这些过程的。 预处理(pre-processing) 例:设计一个程序,要求通过使用gcc的参数,控制深入gcc的编译过程,了解gcc编译灵活性。 分析:(我们使用上一节的例子做直接说明) 1> 先用vim 编辑源程序,生成源程序文1-3.c。2> 然后使用gcc的“-E”参数预处理,生成经过 预处理的的源程序文件1-3.i。 3> 接着使用gcc的“-S”参数编译,生成汇编语 编译(Compiling) 汇编(Assembing) 链接(Linking) 言程序文件1-3.s。 4> 然后用gcc的“- c”参数汇编,生成二进制文件1-3.o。 可执行文件 图 1.4 gcc编译流程 5> 最后使用再一次gcc,把“1-3.o” 和一些用到 的链接库文件链接成可执行文件,并使用“- o”参数,将文件输出到目标文件“1-3”,最终的1-3就是完全编译好的可执行文件。 操作步骤 步骤1:编译程序源程序。 [root@localhost root]# vim 1-2.c 程序代码如下: /*程序1-2.c:华氏温度与摄氏温度对照表*/ #include main() { float fahr , celsius ; /* 定义华氏与摄氏符点型变量 */ fahr = lower ; while (fahr <= upper) { int lower , upper , step ; lower = 0 ; /* 温度表的下限 */ upper = 300 ; /* 温度表的上限 */ step = 20 ; /* 步长 */ celsius = (5.0 / 9.0) = (fahr – 32.0); } } printf(“%3.0f %6.1f\\n”, fahr, celsius); fahr = fahr + step ; 步骤2 :预处理阶段。 在这个阶段,编译器将上述代码中的“stdio.h”编译进来,在此可以用gcc的“-E”参数指定gcc只进行预处理过程。输入如下: [root@localhost root]# gcc –o 1-3.i 1-3.c –E [root@localhost root]#vim 1-3.i 此时,参数“-E”起到让gcc编译时在预处理结束后停止编译过程。“-o”是指向目标文件,成功后,可是使用vi编译器查看预处理结束以后的C原始程序,部分内容如图1.5所示: 图1.5 已经预处理的部分内容 由此可见,gcc确实进行了预处理,把“stdio.h”的内容插入到“1-3.i”文件中。 步骤3 编译阶段 在编译阶段,gcc首先检查代码的规范性,是否有语法错误等,已确定代码实际 要做的工作,在检查没有错误以后,gcc把代码编译成汇编语言。使用参数“-S”指定gcc只进行编译产生汇编代码。输入如下: [root@localhost root]# gcc –o 1-3.s 1-3.i –S [root@localhost root]#vim 1-3.s 此时,使用命令“ls”可是看到1-3.s 文件,后缀为“.s”的文件为汇编原始程序,同样使用vim文本编辑器可以查看其内容:部分内容如图1.6所示: 图1.6 汇编原始代码部分内容 步骤4 汇编阶段 汇编阶段是将前一个阶段(编译阶段)生成的“.s”汇编文件转化成目标文件(二进制代码),使用gcc的参数“-c”让gcc在汇编结束后停止链接过程,吧汇编代码转化成二进制代码,输入如下: [root@localhost root]# gcc –o 1-3.o 1-3.s –c 此时,文件1-3.o中已经是二进制机器代码,我们在使用vim文本编辑器已经无法正确查看其内容(显示为乱码)。部分如图所示: 步骤5 衔接阶段 在C语言的学习中,我们都知道,在程序的源代码中,有时并没有实现全部的函数,比如在例子1-3.c中,就没有函数“printf”实现,在头文件“stdio.h”中只有此函数的声明,也没有函数的实现,那么,此函数是如何实现的呢? 其实,Linux系统把“printf”函数的实现放在了“libc.so.6”的库文件中,在没有参数指定时,gcc将到系统默认的路径“/usr/lib”下查找库文件,找到后,在将函数链接到libc.so.6库函数中去,这样,就有了printf函数的实现部分,把程序中一些函数与这些函数的实现部分链接起来,这就是链接阶段的工作。 完成连接后, gcc 就可以生成可执行程序了,如图1.7所示: [root@localhost root]# gcc –o 1-3 1-3.o 图 1.7链接后生成可执行程序 注意:gcc在编译的时候默认使用动态链接库,使用动态链接库编译链接时并不把库文件 的代码加入到可执行文件中,而是在程序执行的时动态的加载链接库,这样的做的目的是为了节约系统开销。动态链接库的后缀是“。so”,静态是“。a”。 1.2.2 gcc 编辑器的主要参数 gcc有超过100个可用的参数,按照他们功能分类可以分为三大类型:总体参数,警告和出错参数、优化参数。这里主要介绍最常用的参数。 1.总体参数 常用的总体参数如表1.8所示: 表1.8 gcc的总体参数 参数 -c -S -E -g -o file 含义 只是编译不链接,生成目标文件 只是编译不汇编,生成汇编代码 只进行预处理 在可执行程序中包含调试信息 把输出文件输出到file中 参数 -v -I dir -L dir -static 含义 显示gcc的版本 在头文件的搜索路径列表中添加dir 目录 在库文件的搜索路径列表中添加dir目录 链接静态库 -llibrary 连接名为library的库文件 此前,我们已经看到了“-E,-S,-c, -o”的用法,本节就不在过多的介绍了,在此,主要讲解两个常用的库依赖参数“-I dir”,“-L dir”和-l. -I 参数是指头文件的所在目录。使用一个例子就能很好的说明。 例:1-4设计一个程序,要求把输入的字符原样输出并自定义属于自己的头文件,自定义头文件为“my.h”,放在目录“/root/LinuxC”下。 操作步骤 步骤1 :设计编辑源程序代码1-4..c [root@localhost root]# vim 1-4.c 程序源代码如下: #include return 0; 步骤2:设计编辑自定义的头文件my.h [root@localhost root]#mkdir LinuxC [root@localhost root]#cd LinuxC [root@localhost LinuxC]#vim my.h 程序代码如下: /*my.h 自定义头文件*/ #include 步骤3:正常编译1-4. c文件,输入如下: [root@localhost root]# gcc 1-4 –o 1-4.c 首先,gcc会到默认目录“/usr/include”中去寻找“my.h”头文件,为什么呢?,因为在c语言的include语句中,“<>”表示默认路径,如果在include语句中使用了“<>”那么,gcc就会到系统的默认路径中去寻找与两个尖括号(“<”和“>”)中间包含字符的相匹配的头文件,例如,在Linux下,头文件的默认路径是“/usr/include”,那么当C语言include语句中出现“ 此时,我们自定义的头文件在目录“/root/LinuxC”中,当然在默认路径中找不到,程序在编译时会提示出错,表明无法找到头文件的信息。如图1.9: 图1.9 于是,我们就需要使用“-I dir”参数来指定我们的头文件my.h在何位置。 步骤4,使用“-I dir”参数输入如下: [root@localhost root]# gcc 1-4 –o 1-4.c –I /root/LinuxC 编译器就能正确编译,结果如下图2.0: 图2.0 函数执行成功! 参数“-L dir”的功能与“-I dir”类似,能够指定库文件的所在目录,让gcc在库文件的搜索路径列表中添加dir 目录。 参数“llibrary”的使用,前面的“-L dir”,在参数L的后面跟的是文件目录(路径),而没有指定文件,而“l”则是指定特定的库文件!“library”指特定库名。 同样,通过一个例子,我们就可以很好的理解“llibrary”的使用了! 例1-6:设计一个程序,要求把计算输入数字的正弦的值,sin(a)! 操作步骤 步骤1:编辑源程序代码: [root@localhost root]# vim 1-6.c 程序代码如下: /*1-6.c 输入数字,计算数字的正弦值sin(a)*/ #include int main(){ double a,b; printf(“请输入将要计算的数字:\\n”); scanf(“%lf”,&a); b=sin(a); printf(“sin(%lf)=%lf\\n”a,b); } 步骤2:用gcc编译程序。 接着使用gcc编译程序,输入如下: [root@localhost root]# gcc –o 1-6 1-6.c 结果发现编译器报错,如图2.1所示: 图2.1 错误原因主要是没有定义“sin”函数,或者说没有找到“sin”函数的实现,虽然我们在函数开头声明了数学函数库,但还是没有找到sin的实现,这时我们就需要指定sin函数的具体路径了。 在指定具体路径之前,我们当然需要知道这个所谓的具体路径在哪? 技巧:函数的查找方法如下: [root@localhost root]#nm –o /lib/*.so|grep 函数名 可以通过“nm”命令查找我们想要找的函数,例如:sin函数,方法如下: [root@localhost root]#nm –o /lib/*.so|grep sin 这时,查找(部分)结果如下: „„„„„„„„„„„„„„„„„„„„ /lib/libm-2.3.4.so:00008610 W sin /lib/libm-2.3.4.so:00008610 t _sin /lib/libm-2.3.4.so:000183e0 W sinl /lib/libm-2.3.4.so:000183e0 t _sinl „„„„„„„„„„„„„„„„„„„„ 在 /lib/libm-2.3.2.so:00008610 W sin 中,/lib是系统存放函数的默认位置,libm-2.3.2.so是包含sin函数的函数库名,其中,所有函数库的名都以“lib”开头,跟着的字母“m”是包含sin函数的函数库的真正的名子,“-2.3.2”是版本号,“.so”说明它的动态库。 在使用“ - l”参数时,通常的习惯是出去“lib”函数库头和后面的版本号,使用真名和参数“-l”连接,形成“- lm”。于是,我们需要在gcc找不到库时,可是使用“-l”直接给定库名,输入如下: [root@localhost root]# gcc –o 1-6 1-6.c -lm 就可以正确编译了!如图2.2所示: 图2.2 步骤3 :运行程序 编译成功后,就可以执行程序了,结果如图2.3所示: 图2.3程序1-6执行结果 2.告警和出错参数 gcc的常用警告和出错参数如表2.4所示: 参数 -ansi -pedantic -w -Wall -werror 表2.4 gcc的警告和出错参数 含义 支持符合ANSI的C程序 允许发出ANSI C标准所在列的全部警告信息 关闭所有警告 允许发出GCC提供的所有有用的警告信息 把所有的警告信息转化成错误信息,并在告警发生时终止编译 警告和出错参数可以在编译时检查语法的一些规范性,并显示在终端的警告(出错)信息,它对软件工程师编程很有帮助,其中的“-Wall”参数是跟踪调 试的有力武器,在复杂的程序设计中非常有帮助。 例1-7 设计一个程序,要求程序中包含一些非标准语法,熟悉gcc 常用的警告和出错参数的使用: 操作步骤: 步骤1 :设计编辑源程序代码: [root@localhost root]# vim 1-7.c 程序代码如下: /*程序1-7 用于测试警告和出错参数*/ #include return 0; 步骤2 测试1——关闭所有警告 gcc编译时加 “-w”参数输入如下: [root@localhost root]# gcc –o 1-7 1-7.c –w 运行结果如下 图2.5: 图2.5 步骤2 测试2 ——显示不符合ANSI C标准语法的警告信息: gcc编译时加“-ansi”参数,输入如下: [root@localhost root]# gcc –o 1-7 1-7.c –ansi 运行结果如下 图2.6: 图2.6 步骤4 测试3——显示ANSI C 标准所列的全部警告信息: gcc编译时加“-pedantic”参数,输入如下: [root@localhost root]# gcc –o 1-7 1-7.c –pedantic 运行结果如下:图2.7: 图 2.7 步骤5 测试4——显示所有gcc提供的有用的警告 gcc编译时加“-wall”参数输入与结果如下: [root@localhost root]# gcc –o 1-7 1-7.c –Wall gcc的警告和出错参数信息对软件工程师编程非常有帮助,其中的“-wall”参数是跟踪和调试的有用工具,同学们在学习的时候应该养成使用这个参数的习惯,这样在以后进行复杂的程序设计时会节省许多力气。 3. 优化参数 优化参数是指编译器通过分析源代码,找出其中尚未达到最优化的部分,后进行重新整合,目标是改善程序的执行性能。 gcc提供的代码优化功能相当强大,他通过一个正整数与参数“-O”组合而成,正整数就代表需要优化的级别。数字越大,优化的程度也就越高,也就意味值将来编译好的程序运行速度越快,但是编译却要耗费很长时间及大量的系统性能,推荐同学们使用“2”级优化,因为他在优化长度,编译时间和代码大小之间取得了一个比较理想的平衡点。 下面我们结合一个实例来具体看一下优化参数的使用: 例1.8 设计一个程序,要求循环足够多的次数(往往是数亿至数十亿次的循环),每一次循环都要进行乘除的计算,然后使用优化参数对程序进行编译,比较优化前后程序执行的时间。 操作步骤 步骤1 设计编译源代码: [root@localhost root]# vim 1-8.c 程序代码如下: /*1-8 用于测试代码优化的复杂运算程序*/ #include for (testnum = 0 ;testnum < 5000.0 *5000.0 * 5000.0 / 20.0 + temp = testnum/1234; result=testnum; printf(“运算结果是:%lf\\n”,result); 1024;testnum+=(6-2+1)/4 ){ 步骤2 不加优化参数进行编译,输入如下: [root@localhost root]# gcc –o 1-8 1-8.c 编译成功后,我们在程序运行前加“time”程序命令,用于计算程序的运行时间,运行结果如图2.8所示: 图2.8 我们可以看到,上述程序没有优化参数编译后,数亿次的循环需要1分 47秒左右才运行出来,在执行程序在得到结果之前我们确实等待了一段时间! 步骤3 加入优化参数“-O2”编译程序,输入如下: [root@localhost root]# gcc –o 1-8 1-8.c –O2 这时,我们发现,使用优化参数的效果使得编译持续了一段时间。编译成功后,运行程序,同样使用“time”参数计算程序执行时间,运行结果如下图2.9: 图2.9 通过对比我们发现,经过优化参数编译后,从程序执行到得到执行结果,确实缩短了程序执行时间,从原来的1分 47秒缩减到现在的31秒,效率提高了将近3倍。程序的性能通过优化参数得到很大的改善和提高。 尽管gcc 优化代码的功能十分强大,但最为一名优秀的软件工程师,首先还是要力求自己写出高质量的代码,任何事物都有有利有弊,不能完全的形成依赖,还要权衡用之。 1.3 gdb调试器 罗马不是一天就建成的,这句话同样适用于软件开发,任何优秀的软件工师都不敢保证说他的代码一次就可以通过执行,其实软件开发大部分就是一个代码反复调试的过程,这时候,为了优化代码,快速定位程序中出现的问题等,我们就需要对代码进行调试,在命令行模式下,Linux提供给我们一个非常好用的调试器:gdb。 1.3.1 gdb概述 Linux下的gdb调试器,是一款GNU组织开发并发布的在UNIX/Linux 下的程序调试工具,虽然他没有图形化的界面,但是它的功能也可以说是相当强大,足以很多商业化的调试工具相媲美。 区别于其他商业化的调试工具,gdb 调试的是可执行程序,而不是程序源文件,因此,使用gdb 前必须先编译源文件。在上一节中的gcc总体参数中,有一个参数“- g”,只有编译时的时候加入这个参数,生成的可执行文件才包含调试信息,否则gdb无法加载该可执行程序,也就无法进行程序的调试。 1.3.2 使用gdb调试程序 常用gdb命令如表3.0所示: 表3.0常用gdb命令 命令格式 list<行号|函数名> break 行号|函数名|表达式 info braek Run print 表达式|变量 Next step continue 作用 查看指定位置的程序代码 设置断点 显示断点信息 运行程序 查看运行时表达式和变量的值 步入,不 进入函数调用 步入,进入函数调用 继续执行,只到函数结束或遇到新的断点 下面,我们就通过一个例子,简单的说明gdb调试器的用法。 例1-9,设计一个程序,要求至少有一个自定义函数。 步骤1:设计源程序代码 [root@localhost root]#vim 1-9.c 程序代码如下: #include int min(int x,int y ); int main(){ int a1,a2,min_int; printf(\"请输入第一个整数:\"); scanf(\"%d\ printf(\"请输入第二个整数:\"); scanf(\"%d\ min_int = min(a1,a2) printf(\"两个数之间的最小整数是:%d\\n\} int min(int x, int y){ if(x } 步骤2 用gcc编译程序 注意:我们在编译的时候一定要加上选项“-g”,这样,编译出来的代码才包含调试信息,否则在运行gdb后无法载入该执行文件。 [root@localhost root]#gcc –o 1-9 1-9.c -g 步骤3 进入gdb调试环境。 gdb进行调试的是可执行文件,因此要调试的是1-9,而不是1-9.c [root@localhost root]#gdb 1-9 回车后就进入了gdb调式模式,如图所示: 可以看到gdb的启动画面中有gdb的版本号,使用的库文件等信息,在gdb的调 试环境中,提示符是“(gdb)”。 gdb小试牛刀:1>查看源文件:命令“list”,输入“l”就可以了如图: 可以看出,gdb列出的源代码中明确给出了对应的行号,这样可以大大方便代码的定位。 提示:如果我们要查看指定位置的代码,只需要使用命令“list ‘行号‘”就可以了。 2> 设置断点 设置断点是调试程序的一个非常重要的手段,它使程序到一定位置暂停程序,工程师可以在断点或单步前进的时候查看变量的值,设置断点的命令是“break ‘行号’”。或者“b ‗行号‘”.如图: 断点设置好以后,我们就可以使用“run”程序来开始调试程序了! 3>查看变量 查看变量才是程序调试的重要手段,程序运行到断点处会暂停,此时,输入“p ‗变量名‘”可查看指定变量的值。如图所示: 4>:单步运行。 单步运行可以使用s(step)命令或n(next)命令,他们俩的区别在于,当程序步进到函数时,“s”会进入该函数,“n”则不会进入该函数。 S步进: n步进: 5>,继续运行程序命令“c”(continue),跳出补进,可以让我们一口气一直运行到整个程序的最末端。 最后,退出gdb程序,“q”(quit)。 以上就是一个演示gdb的例子,仅仅是演示而已,(我们的程序没有错,当然不需要调试),有兴趣的同学可以下去自己信息实验一下,体会一下我们的基于命令行的调试程序和平常所用的基于图形的调试程序有何不同。 1.4 make文件管理器 到此为止,我们已经了解了如何在Linux下使用编辑器编写代码,如何使用gcc把代码编译成可执行文件,还学习了如何使用gdb来调试程序,那么,所有的工作看似已经完成了,为什么还需要Make这个工程管理器呢? 所谓工程管理器,顾名思义,是指管理较多的文件的。试想一下,有一个上百个文件的代码构成的项目,如果其中只有一个或少数几个文件进行了修改,按照之前所学的gcc编译工具,就不得不把这所有的文件重新编译一遍,因为编译器并不知道哪些文件是最近更新的,而只知道需要包含这些文件才能把源代码编译成可执行文件,于是,程序员就不能不再重新输入数目如此庞大的文件名以完成最后的编译工作。 但是,请仔细回想一下我们在前面所阐述的编译过程,编译过程是分为编译、汇编、链接不同阶段的,其中编译阶段仅检查语法错误以及函数与变量的声明是否正确声明了,在链接阶段则主要完成是函数链接和全局变量的链接。因此,那些没有改动的源代码根本不需要重新编译,而只要把它们重新链接进去就可以了。所以,人们就希望有一个工程管理器能够自动识别更新了的文件代码,同时又不需要重复输入冗长的命令行,这样,Make工程管理器也就应运而生了! 实际上,Make工程管理器也就是个“自动编译管理器”,这里的“自动”是指它能够根据文件时间戳自动发现更新过的文件而减少编译的工作量,同时,它通过读入Makefile文件的内容来执行大量的编译工作。用户只需编写一次简单的编译语句就可以了。它大大提高了实际项目的工作效率,而且几乎所有Linux下的项目编程均会涉及到它。 1.4.1.1 Makefile基本结构 Makefile是Make读入的惟一配置文件,因此本节的内容实际就是讲Makefile的编写规则。在一个Makefile中通常包含如下内容: · 需要由make工具创建的目标体(target),通常是目标文件或可执行文件; · 要创建的目标体所依赖的文件(dependency_file); · 创建每个目标体时需要运行的命令(command)。 它的格式为: target: dependency_files command #The simplest example hello.o: hello.c hello.h gcc hello.c –o hello.o 接着就可以使用make了。使用make的格式为:make target,这样make就会自动读入Makefile(也可以是首字母小写makefile)并执行对应target的command语句,并会找到相应的依赖文件。如下所示: [root@localhost makefile]# make hello.o gcc hello.c –o hello.o [root@localhost makefile]# ls hello.c hello.h hello.o Makefile 可以看到,Makefile执行了“hello.o”对应的命令语句,并生成了“hello.o”目标体 1.4..2 Makefile变量 上面示例的Makefile在实际中是几乎不存在的,因为它过于简单,仅包含两个文件和一个命令,在这种情况下完全不必要编写Makefile而只需在Shell中直接输入即可,在实际中使用的Makefile往往是包含很多的文件和命令的,这也是Makefile产生的原因。下面就可给出稍微复杂一些的Makefile进行讲解。 sunq:kang.o yul.o Gcc kang.o bar.o -o sunq kang.o : kang.c kang.h head.h Gcc –Wall –O -g –c kang.c -o kang.o yul.o : bar.c head.h Gcc - Wall –O -g –c yul.c -o yul.o 在这个Makefile中有三个目标体(target),分别为sunq、kang.o和yul.o,其中第一个目标体的依赖文件就是后两个目标体。如果用户使用命令“make sunq”,则make管理器就是找到sunq目标体开始执行。 这时,make会自动检查相关文件的时间戳。首先,在检查“kang.o”、“yul.o”和“sunq”三个文件的时间戳之前,它会向下查找那些把“kang.o”或“yul.o”做为目标文件的时间戳。比如,“kang.o”的依赖文件为:“kang.c”、“kang.h”、“head.h”。如果这些文件中任何一个的时间戳比“kang.o”新,则命令“gcc –Wall –O -g –c kang.c -o kang.o”将会执行,从而更新文件“kang.o”。在更新完“kang.o”或“yul.o”之后,make会检查最初的“kang.o”、“yul.o”和“sunq”三个文件,只要文件“kang.o”或“yul.o”中的任比文件时间戳比“sunq”新,则第二行命令就会被执行。这样,make就完成了自动检查时间戳的工作,开始执行编译工作。这也就是Make工作的基本流程。 接下来,为了进一步简化编辑和维护Makefile,make允许在Makefile中创建和使用变量。变量是在Makefile中定义的名字,用来代替一个文本字符串,该文本字符串称为该变量的值。在具体要求下,这些值可以代替目标体、依赖文件、命令以及makefile文件中其它部分。在Makefile中的变量定义有两种方式:一种是递归展开方式,另一种是简单方式。 递归展开方式定义的变量是在引用在该变量时进行替换的,即如果该变量包含了对其他变量的应用,则在引用该变量时一次性将内嵌的变量全部展开,虽然这种类型的变量能够很好地完成用户的指令,但是它也有严重的缺点,如不能在变量后追加内容(因为语句:CFLAGS = $(CFLAGS) -O在变量扩展过程中可能导致无穷循环)。 为了避免上述问题,简单扩展型变量的值在定义处展开,并且只展开一次,因此它不包含任何对其它变量的引用,从而消除变量的嵌套引用。 递归展开方式的定义格式为:VAR=var 简单扩展方式的定义格式为:VAR:=var Make中的变量使用均使用格式为:$(VAR) 变量名是不包括“:”、“#”、“=”结尾空格的任何字符串。同时,变量名中包含字母、数字以及下划线以外的情况应尽量避免,因为它们可能在将来被赋予特别的含义。 变量名是大小写敏感的,例如变量名“foo”、“FOO”、和“Foo”代表不同的变量。 推荐在makefile内部使用小写字母作为变量名,预留大写字母作为控制隐含规则参数或用户重载命令选项参数的变量名。 注意 下面给出了上例中用变量替换修改后的Makefile,这里用OBJS代替kang.o和yul.o,用CC代替Gcc,用CFLAGS代替“-Wall -O –g”。这样在以后修改时,就可以只修改变量定义,而不需要修改下面的定义实体,从而大大简化了Makefile维护的工作量。 经变量替换后的Makefile如下所示: OBJS = kang.o yul.o CC = Gcc CFLAGS = -Wall -O -g sunq : $(OBJS) $(CC) $(OBJS) -o sunq kang.o : kang.c kang.h $(CC) $(CFLAGS) -c kang.c -o kang.o yul.o : yul.c yul.h $(CC) $(CFLAGS) -c yul.c -o yul.o 可以看到,此处变量是以递归展开方式定义的。 Makefile中的变量分为用户自定义变量、预定义变量、自动变量及环境变量。如上例中的OBJS就是用户自定义变量,自定义变量的值由用户自行设定,而预定义变量和自动变量为通常在Makefile都会出现的变量,其中部分有默认值,也就是常见的设定值,当然用户可以对其进行修改。 预定义变量包含了常见编译器、汇编器的名称及其编译选项。下表1.4.1列出了Makefile中常见预定义变量及其部分默认值。 表1.4.1 Makefile中常见预定义变量 命 令 格 式 AR AS CC CPP CXX FC RM ARFLAGS ASFLAGS CFLAGS 含 义 库文件维护程序的名称,默认值为ar 汇编程序的名称,默认值为as C编译器的名称,默认值为cc C预编译器的名称,默认值为$(CC) –E C++编译器的名称,默认值为g++ FORTRAN编译器的名称,默认值为f77 文件删除程序的名称,默认值为rm –f 库文件维护程序的选项,无默认值 汇编程序的选项,无默认值 C编译器的选项,无默认值 CPPFLAGS CXXFLAGS FFLAGS C预编译的选项,无默认值 C++编译器的选项,无默认值 FORTRAN编译器的选项,无默认值 可以看出,上例中的CC和CFLAGS是预定义变量,其中由于CC没有采用默认值,因此,需要把“CC=Gcc”明确列出来。 由于常见的Gcc编译语句中通常包含了目标文件和依赖文件,而这些文件在Makefile文件中目标体的一行已经有所体现,因此,为了进一步简化Makefile的编写,就引入了自动变量。自动变量通常可以代表编译语句中出现目标文件和依赖文件等,并且具有本地含义(即下一语句中出现的相同变量代表的是下一语句的目标文件和依赖文件)。下表1.4.2列出了Makefile中常见自动变量。 命令格式 $* $+ $< $? $@ $^ $% 含 义 不包含扩展名的目标文件名称 所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件 第一个依赖文件的名称 所有时间戳比目标文件晚的依赖文件,并以空格分开 目标文件的完整名称 所有不重复的依赖文件,以空格分开 如果目标是归档成员,则该变量表示目标的归档成员名称 表1.4.2 Makefile中常见自动变量 自动变量的书写比较难记,但是在熟练了之后会非常的方便,请结合下例中的自动变量改写的Makefile进行记忆。 OBJS = kang.o yul.o CC = Gcc CFLAGS = -Wall -O -g sunq : $(OBJS) $(CC) $^ -o $@ kang.o : kang.c kang.h $(CC) $(CFLAGS) -c $< -o $@ yul.o : yul.c yul.h $(CC) $(CFLAGS) -c $< -o $@ 另外,在Makefile中还可以使用环境变量。使用环境变量的方法相对比较简单,make在启动时会自动读取系统当前已经定义了的环境变量,并且会创建与之具有相同名称和数值的变量。但是,如果用户在Makefile中定义了相同名称的变量,那么用户自定义变量将会覆盖同名的环境变量 1.4.3 Makefile规则 Makefile的规则是Make进行处理的依据,它包括了目标体、依赖文件及其之间的命令语句。一般的,Makefile中的一条语句就是一个规则。在上面的例子中,都显示地指出了Makefile中的规则关系,如“$(CC) $(CFLAGS) -c $< -o $@”,但为了简化Makefile的编写,make还定义了隐式规则和模式规则,下面就分别对其进行讲解。 1.隐式规则 隐含规则能够告诉make怎样使用传统的技术完成任务,这样,当用户使用它们时就不必详细指定编译的具体细节,而只需把目标文件列出即可。Make会自动搜索隐式规则目录来确定如何生成目标文件。如上例就可以写成: OBJS = kang.o yul.o CC = Gcc CFLAGS = -Wall -O -g sunq : $(OBJS) $(CC) $^ -o $@ 为什么可以省略后两句呢?因为Make的隐式规则指出:所有“.o”文件都可自动由“.c”文件使用命令“$(CC) $(CPPFLAGS) $(CFLAGS) -c file.c –o file.o”生成。这样“kang.o”和“yul.o”就会分别调用“$(CC) $(CFLAGS) -c kang.c -o kang.o”和“$(CC) $(CFLAGS) -c yul.c -o yul.o”生成。 注意 在隐式规则只能查找到相同文件名的不同后缀名文件,如”kang.o”文件必须由”kang.c”文件生成。 下表1.4.3给出了常见的隐式规则目录: 表1.4.3 Makefile中常见隐式规则目录 对应语言后缀名 C编译:.c变为.o C++编译:.cc或.C变为.o 规 则 $(CC) –c $(CPPFLAGS) $(CFLAGS) $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) Pascal编译:.p变为.o Fortran编译:.r变为-o $(PC) -c $(PFLAGS) $(FC) -c $(FFLAGS) 2.模式规则 模式规则是用来定义相同处理规则的多个文件的。它不同于隐式规则,隐式规则仅仅能够用make默认的变量来进行操作,而模式规则还能引入用户自定义变量,为多个文件建立相同的规则,从而简化Makefile的编写。 模式规则的格式类似于普通规则,这个规则中的相关文件前必须用“%”标明。使用模式规则修改后的Makefile的编写如下: OBJS = kang.o yul.o CC = Gcc CFLAGS = -Wall -O -g sunq : $(OBJS) $(CC) $^ -o $@ %.o : %.c $(CC) $(CFLAGS) -c $< -o $@ 1.4.4 Make使用 使用make管理器非常简单,只需在make命令的后面键入目标名即可建立指定的目标,如果直接运行make,则建立Makefile中的第一个目标。 此外make还有丰富的命令行选项,可以完成各种不同的功能。 下表1.4.4列出了常用的make命令行选项。 表1.4.4 make的命令行选项 命令格式 -C dir -f file -i -I dir -n -p -s -w 含 义 读入指定目录下的Makefile 读入当前目录下的file文件作为Makefile 忽略所有的命令执行错误 指定被包含的Makefile所在目录 只打印要执行的命令,但不执行这些命令 显示make变量数据库和隐含规则 在执行命令时不显示命令 如果make在执行过程中改变目录,则打印当前目录名 好,下面我们就拿一下真的工程来看一下Makefile文件在实际当中的应用。 下面的代码是我们会经常看到的Makefile。 # MA 02111-1307 USA VERSION = 1 PATCHLEVEL = 2 SUBLEVEL = 0 EXTRAVERSION = U_BOOT_VERSION = $(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION) VERSION_FILE = $(obj)include/version_autogenerated.h HOSTARCH := $(shell uname -m | \\ HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' | \\ export HOSTARCH HOSTOS # Deal with colliding definitions from tcsh etc. VENDOR= # the object files are placed in the source directory. # ifdef O ifeq (\"$(origin O)\BUILD_DIR := $(O) endif endif ifneq ($(BUILD_DIR),) sed -e 's/\\(cygwin\\).*/cygwin/') sed -e s/i.86/i386/ \\ -e s/sun4u/sparc64/ \\ -e s/arm.*/arm/ \\ -e s/sa110/arm/ \\ -e s/powerpc/ppc/ \\ -e s/macppc/ppc/) saved-output := $(BUILD_DIR) # Attempt to create a output directory. $(shell [ -d ${BUILD_DIR} ] || mkdir -p ${BUILD_DIR}) # Verify if it was successful. BUILD_DIR := $(shell cd $(BUILD_DIR) && /bin/pwd) $(if $(BUILD_DIR),,$(error output directory \"$(saved-output)\" does not exist)) endif # ifneq ($(BUILD_DIR),) OBJTREE SRCTREE TOPDIR LNDIR MKCONFIG := $(SRCTREE)/mkconfig export MKCONFIG ifneq ($(OBJTREE),$(SRCTREE)) REMOTE_BUILD := 1 export REMOTE_BUILD endif # $(obj) and (src) are defined in config.mk but here in main Makefile # we also need them before config.mk is included which is the case for # some targets like unconfig, clean, clobber, distclean, etc. ifneq ($(OBJTREE),$(SRCTREE)) obj := $(OBJTREE)/ src := $(SRCTREE)/ else obj := src := := $(if $(BUILD_DIR),$(BUILD_DIR),$(CURDIR)) := $(CURDIR) := $(SRCTREE) := $(OBJTREE) export TOPDIR SRCTREE OBJTREE endif export obj src ################################################################## ifeq # load ARCH, BOARD, and CPU configuration include $(OBJTREE)/include/config.mk export ARCH CPU BOARD VENDOR SOC CROSS_COMPILE = /armtools/bin/arm-linux- ifndef CROSS_COMPILE ifeq ($(HOSTARCH),ppc) CROSS_COMPILE = else ifeq ($(ARCH),ppc) CROSS_COMPILE = powerpc-linux- endif ifeq ($(ARCH),arm) CROSS_COMPILE = arm-linux- Endif endif endif export CROSS_COMPILE # load other configuration include $(TOPDIR)/config.mk ################################################################### OBJS = cpu/$(CPU)/start.o ifeq ($(CPU),i386) OBJS += cpu/$(CPU)/start16.o OBJS += cpu/$(CPU)/reset.o endif ($(OBJTREE)/include/config.mk,$(wildcard $(OBJTREE)/include/config.mk)) ifeq ($(CPU),ppc4xx) OBJS += cpu/$(CPU)/resetvec.o endif ifeq ($(CPU),mpc85xx) OBJS += cpu/$(CPU)/resetvec.o endif ifeq ($(CPU),mpc86xx) OBJS += cpu/$(CPU)/resetvec.o endif ifeq ($(CPU),bf533) OBJS += cpu/$(CPU)/start1.o cpu/$(CPU)/interrupt.o OBJS := $(addprefix $(obj),$(OBJS)) LIBS = lib_generic/libgeneric.a LIBS += board/$(BOARDDIR)/lib$(BOARD).a LIBS += cpu/$(CPU)/lib$(CPU).a ifdef SOC LIBS += cpu/$(CPU)/$(SOC)/lib$(SOC).a endif LIBS += lib_$(ARCH)/lib$(ARCH).a LIBS += fs/cramfs/libcramfs.a fs/fat/libfat.a fs/fdos/libfdos.a fs/jffs2/libjffs2.a \\ fs/reiserfs/libreiserfs.a fs/ext2/libext2fs.a LIBS += net/libnet.a LIBS += disk/libdisk.a LIBS += rtc/librtc.a LIBS += dtt/libdtt.a LIBS += drivers/libdrivers.a LIBS += drivers/nand/libnand.a cpu/$(CPU)/cache.o cpu/$(CPU)/flush.o OBJS += cpu/$(CPU)/cplbhdlr.o cpu/$(CPU)/cplbmgr.oendif LIBS += drivers/nand_legacy/libnand_legacy.a ifeq ($(CPU),mpc83xx) LIBS += drivers/qe/qe.a endif LIBS += drivers/sk98lin/libsk98lin.a LIBS += post/libpost.a post/cpu/libcpu.a LIBS += common/libcommon.a LIBS += $(BOARDLIBS) LIBS := $(addprefix $(obj),$(LIBS)) .PHONY : $(LIBS) # Add GCC lib PLATFORM_LIBS SUBDIRS ifeq ($(CONFIG_NAND_U_BOOT),y) NAND_SPL = nand_spl U_BOOT_NAND = $(obj)u-boot-nand.bin endif __OBJS := $(subst $(obj),,$(OBJS)) __LIBS := $(subst $(obj),,$(LIBS)) ################################################################## ALL all: = $(obj)u-boot.srec $(ALL) $(obj)u-boot.bin $(obj)System.map $(U_BOOT_NAND) $(obj)u-boot.hex: $(obj)u-boot $(OBJCOPY) ${OBJCFLAGS} -O ihex $< $@ += -L $(shell dirname `$(CC) $(CFLAGS) -print-libgcc-file-name`) -lgcc = tools \\ examples \\ post \\ post/cpu .PHONY : $(SUBDIRS) $(obj)u-boot.srec: $(obj)u-boot $(OBJCOPY) ${OBJCFLAGS} -O srec $< $@ $(OBJCOPY) ${OBJCFLAGS} -O binary $< $@ ./tools/mkimage -A $(ARCH) -T firmware -C none \\ -a $(TEXT_BASE) -e 0 \\ -n $(shell sed -n -e 's/.*U_BOOT_VERSION//p' $(VERSION_FILE) | \\ sed -e 's/\"[ ]*$$/ for $(BOARD) board\"/') \\ -d $< $@ $(OBJDUMP) -d $< > $@ depend version $(SUBDIRS) $(OBJS) $(LIBS) $(LDSCRIPT) -x $(LIBS) |sed -n -e UNDEF_SYM=`$(OBJDUMP) $(obj)u-boot.bin: $(obj)u-boot $(obj)u-boot.img: $(obj)u-boot.bin $(obj)u-boot.dis: $(obj)u-boot $(obj)u-boot: 's/.*\\(__u_boot_cmd_.*\\)/-u\\1/p'|sort|uniq`;\\ cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS) \\ --start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \\ -Map u-boot.map -o u-boot $(OBJS): $(MAKE) -C cpu/$(CPU) $(if $(REMOTE_BUILD),$@,$(notdir $@)) $(MAKE) -C $(dir $(subst $(obj),,$@)) $(MAKE) -C $@ all $(MAKE) -C nand_spl/board/$(BOARDDIR) all cat $(obj)nand_spl/u-boot-spl-16k.bin $(obj)u-boot.bin > $(LIBS): $(SUBDIRS): $(NAND_SPL): version $(U_BOOT_NAND): $(NAND_SPL) $(obj)u-boot.bin $(obj)u-boot-nand.bin version: \\ @echo -n \"#define U_BOOT_VERSION \\\"U-Boot \" > $(VERSION_FILE); \\ echo -n \"$(U_BOOT_VERSION)\" >> $(VERSION_FILE); \\ echo -n $(shell $(CONFIG_SHELL) $(TOPDIR)/tools/setlocalversion $(TOPDIR)) >> $(VERSION_FILE); \\ echo \"\\\"\" >> $(VERSION_FILE) $(MAKE) -C tools/gdb all || exit 1 $(MAKE) -C tools/updater all || exit 1 $(MAKE) -C tools/env all || exit 1 for dir in $(SUBDIRS) ; do $(MAKE) -C $$dir _depend ; done ctags -w -o $(OBJTREE)/ctags `find $(SUBDIRS) include \\ lib_generic board/$(BOARDDIR) cpu/$(CPU) lib_$(ARCH) \\ fs/cramfs fs/fat fs/fdos fs/jffs2 \\ net disk rtc dtt drivers drivers/sk98lin common \\ gdbtools: updater: env: depend dep: tags ctags: \\( -name CVS -prune \\) -o \\( -name '*.[ch]' -print \\)` etags: etags -a -o $(OBJTREE)/etags `find $(SUBDIRS) include \\ lib_generic board/$(BOARDDIR) cpu/$(CPU) lib_$(ARCH) \\ fs/cramfs fs/fat fs/fdos fs/jffs2 \\ net disk rtc dtt drivers drivers/sk98lin common \\ \\( -name CVS -prune \\) -o \\( -name '*.[ch]' -print \\)` $(obj)System.map: $(obj)u-boot @$(NM) $< | \\ grep -v '\\(compiled\\)\\|\\(\\.o$$\\)\\|\\( [aUw] \\)\\|\\(\\.\\.ng$$\\)\\|\\(LASH[RL]DI\\)' | \\ sort > $(obj)System.map ################################################################## else all $(obj)u-boot.hex $(obj)u-boot.srec $(obj)u-boot.bin \\ $(obj)u-boot.img $(obj)u-boot.dis $(obj)u-boot \\ $(SUBDIRS) version gdbtools updater env depend \\ dep tags ctags etags $(obj)System.map: @echo \"System not configured - see README\" >&2 @ exit 1 endif .PHONY : CHANGELOG CHANGELOG: git log --no-merges U-Boot-1_1_5.. | \\ unexpand -a | sed -e 's/\\s\\s*$$//' > $@ ################################################################### unconfig: @rm -f $(obj)include/config.h $(obj)include/config.mk \\ $(obj)board/*/config.tmp $(obj)board/*/*/config.tmp =================================================================== # PowerPC #================================================================= ################################################################### ## MPC5xx Systems #################################################################### canmb_config: unconfig smdk2410_config : unconfig @$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 NULL s3c24x0 „„ @$(MKCONFIG) -a canmb ppc mpc5xxx canmb unconfig @$(MKCONFIG) $(@:_config=) ppc mpc5xx cmi unconfig @$(MKCONFIG) $(@:_config=) ppc mpc5xx pati mpl cmi_mpc5xx_config: PATI_config: 3.7 使用autotools 在上一小节,读者已经了解到了make 项目管理器的强大功能。的确,Makefile 可以帮助make 完成它的使命,但要承认的是,编写Makefile 确实不是一件轻松的事,尤其对于一个较大的项目而言更是如此。那么,有没有一种轻松的手段生成Makefile而同时又能让用户享受make 的优越性呢?本节要讲的autotools 系列工具正是为此而设的,它只需用户输入简单的目标文件、依赖文件、文件目录等就可以轻松地生成Makefile了,这无疑是广大用户的所希望的。另外,这 些工具还可以完成系统配置信息的收集,从而可以方便地处理各种移植性的问题。也正是基于此,现在Linux 上的软件开发一般都用autotools 来制作Makefile,读者在后面的讲述中就会了解到。 3.7.1 autotools使用流程 正如前面所言,autotools 是系列工具,读者首先要确认系统是否装了以下工具(可以用which命令进行查看)。 aclocal autoscan autoconf autoheader automake 使用autotools主要就是利用各个工具的脚本文件以生成最后的Makefile。其总体流程是这样的。 使用aclocal生成一个“aclocal.m4”文件,该文件主要处理本地的宏定义;改写“configure.scan”文件,并将其重命名为“configure.in”,并使用autoconf 文件生成configure文件。 接下来,笔者将通过一个简单的hello.c例子带领读者熟悉autotools生成makefile的过程,由于在这过程中有涉及较多的脚本文件,为了更清楚地了解相互之间的关系,强烈建议读者实际动手操作以体会其整个过程。 1.autoscan 它会在给定目录及其子目录树中检查源文件,若没有给出目录,就在当前目录及其子目录树中进行检查。它会搜索源文件以寻找一般的移植性问题并创建一个文件“configure.scan”,该文件就是接下来autoconf要用到的“configure.in”原型。如下所示: 由上述代码可知autoscan首先会尝试去读入“configure.ac”(同configure.in的配置文件)文件,此时还没有创建该配置文件,于是它会自动生成一个“configure.in”的原型文件configure.scan”。 2.autoconf configure.in是autoconf的脚本配置文件,它的原型文件“configure.scan”如下所示: 下面对这个脚本文件进行解释。 以“#”号开始的行为注释。 AC_PREREQ 宏声明本文件要求的autoconf版本,如本例使用的版本2.59。 AC_INIT宏用来定义软件的名称和版本等信息,在本例中省略了AM_INIT_AUTOMAKE 是自己另加的,它是automake所必备的宏,也同前面一AC_CONFIG_SRCDIR宏用来侦测所指定的源码文件是否存在,来确定源码目AC_CONFIG_HEADER宏用于生成config.h文件,以便autoheader 使用。 AC_CONFIG_FILES宏用于生成相应的Makefile 文件。 中间的注释间可以添加分别用户测试程序、测试函数库、测试头文件等宏定 BUG-REPORT-ADDRESS,一般为作者的E-mail。 样,PACKAGE 是所要产生软件套件的名称,VERSION 是版本编号。 录的有效性。在此处为当前目录下的hello.c。 义。 接下来首先运行aclocal,生成一个“aclocal.m4”文件,该文件主要处理本地的宏定义。如下所示: 再接着运行autoconf,生成“configure”可执行文件。如下所示: 3.autoheader 接着使用autoheader 命令,它负责生成config.h.in文件。该工具通常会从“acconfig.h”文件中复制用户附加的符号定义,因此此处没有附加符号定义,所以不需要创建“acconfig.h”文件。如下所示: 4.automake 这一步是创建Makefile 很重要的一步,automake 要用的脚本配置文件是Makefile.am,用户需要自己创建相应的文件。之后,automake工具转换成Makefile.in。在该例中,笔者创建的文件为Makefile.am如下所示: 下面对该脚本文件的对应项进行解释。 已 经有所介绍)对自己发布的软件有严格的规范,比如必须附带许可证声明文件COPYING 等,否则automake执行时会报错。automake提供了3 种软件等级:foreign、gnu和gnits,让用户选择采用,默认等级为gnu。在本例使用foreign等级,它只检测必须的文件。 bin_PROGRAMS定义要产生的执行文件名。如果要产生多个执行文件,每个hello_SOURCES 定义“hello”这个执行程序所需要的原始文件。如果“hello”文件名用空格隔开。 这个程序是由多个原始文件所产生的,则必须把它所用到的所有原始文件都列出来,并用空格隔开。例如:若目标体“hello”需要“hello.c”、“sunq.c”、“hello.h”三个依赖文件,则定义hello_SOURCES=hello.c sunq.c hello.h。要注意的是,如果要定义多个执行文件,则对每个执行程序都要定义相应的file_SOURCES。 接下来可以使用automake 对其生成“configure.in”文件,在这里使用选项 —adding-missing”可以让automake自动添加有一些必需的脚本文件。如下所示: 其中的AUTOMAKE_OPTIONS为设置automake 的选项。由于GNU(在第1章中 可以看到,在automake之后就可以生成configure.in文件。 5.运行configure 在这一步中,通过运行自动配置设置文件configure,把Makefile.in 变成了最终的Makefile。如下所示: 可以看到,在运行configure 时收集了系统的信息,用户可以在configure 命令中对其进行方便地配置。在./configure 的自定义参数有两种,一种是开关式(--enable-XXX 或--disable-XXX),另一种是开放式,即后面要填入一串字符(--with-XXX=yyyy)参数。读者可以自行尝试其使用方法。另外,读者可以查看同一目录下的“config.log”文件,以方便调试之用。 到此为止,makefile就可以自动生成了。回忆整个步骤,用户不再需要定制不同的规则,而只需要输入简单的文件及目录名即可,这样就大大方便了用户的使用。autotools 生成Makefile流程图如下图所示。 3.7.2 使用autotools所生成的Makefile autotools生成的Makefile除具有普通的编译功能外,还具有以下主要功能(感兴趣的读者可以查看这个简单的hello.c程序的makefile)。 1.make 键入make默认执行“make all”命令,即目标体为all,其执行情况如下所示: 此时在本目录下就生成了可执行文件“hello”,运行“./hello”能出现正常结果,如下所示: 2.make install 此时,会把该程序安装到系统目录中去,如下所示: 此时,若直接运行hello,也能出现正确结果,如下所示: 3.make clean 此时,make会清除之前所编译的可执行文件及目标文件(object file, *.o),如下所示: 4.make dist 此时,make将程序和相关的文档打包为一个压缩文档以供发布,如下所示: 可见该命令生成了一个hello-1.0-tar.gz 的压缩文件。 由上面的讲述读者不难看出,autotools 确实是软件维护与发布的必备工具,鉴于此,如今GUN 的软件一般都是由automake来制作的。 第二章 文件操作 2.1 Linux文件系统 文件和文件系统都是操作系统的重要概念。 文件是指有名字的一组相关信息的集合,文件系统是指按照一定规律组织起来的有序的文件组织结构,是构成多有数据的基础,系统的所有文件都是驻留在文件系统中的某一特定位置。 Linux系统中,文件的准确定义是不包含有任何其他结构的字符流,换句话说,文件中的字符与字符之间除了同属一个文件之外,不存在任何关系,文件中字符间的关系,使用文件的应用程序来建立和解释。 Linux系统提供的文件系统,是树形层次结构的系统,最顶层是根“/”,然后在下层创建其他目录,所有的文件最终都归属到最顶层的根目录“/”,Linux支持多种文件系统,最常用的文件系统是etx2(etx3)系统。 每个文件都具有特定的属性,Linux系统的文件属性比较复杂,主要包括文件类型和文件权限两个方面。 在Linux中,最常见的文件类型有五种,他们是普通文件,目录文件,衔接文件,管道文件,和设备文件。 2.1.1 Linux文件权限 Linux系统是一个典型的多用户系统,不同的用户处于不同的地位,为了保护系统的安全性,Linux系统对不同的用户访问同一文件的权限做了不同的规定。 对于Linux系统中的文件来说,它的权限可以分为 四种:可读,可写,可执行和无权限,分别用“r”,“ w”, “x”, “-”表示。 在Linux中,对于任意一个文件来说,它都有一个特定的所有者,就是这个文件的创造者,同时,Linux系统是按照组分类的,一个用户属于一个或多个组,文件所有者意外的用户,又可以分为文件所有者的同组用户和其他用户,因此Linux按照文件所有者,文件所有者同组用户和其他用户三类规定不同的文件访问权限。 在查看文件的所有详细信息时,可以看到文件显示的作为文件权限的10个字符,可分为四部分: 如图2-1-1! 2-1-1 文件的权限 如图所示:前面的“-r-xr-xr-x“就是文件的权限部分。 第一位:表示文件类型.(“-” 普通文件,“l” 链接文件 “b”设备文件 “p” 管道文件,“d” 目录文件) 第一位到第四位:(rwx)表示文件所有者的访问权限。 第五位到第七位:(rwx)表示文件所有者同组用户的访问权限。 第八位到第十位:(rwx)表示文件所有者同组用户的访问权限。 2.1.2 文件的相关信息 在Linux系统中,与文件相关的新有有三项,他们是:文件的目录结构,索引节点和数据本身。 1.文件的目录结构: 系统中的每一个目录都处于一定的目录结构中,该结构含有目录中所有的目录项的列表,每一个目录项都含有一个名称和索引节点,借助于名称可以访问目录项的内容,而索引节点号则提供了所需引用文件自身的信息。 2.索引节点 在Linux系统中,所有的文件都有一个与之相连的索引节点(inode),索引节点是用来保存文件信息的,它包含以下信息: 文件使用的设备号; 索引节点号; 文件访问权限; 衔接到此文件的目录树; 所有者用户识别号; 组识别号; 设备文件的设备号; 以字节为单位的文件容量 包含该文件的磁盘块的大小; 该文件所占的磁盘块数; 最后一次访问该文件的时间; 最后一次修改该文件的时间; 最后一次改变了文件状态的时间。 在系统中定义了stat结构体来存放这些信息,stat结构的定义如下: Struct stat{ dev_t st_dev; /*文件使用的设备号*/ ino_t st_ino; /*节点号 */ mode_t st_mode; /*文件权限*/ nlink_t st_nlink; /*衔接到此文件的目录树*/ size_t st_size /*文件大小*/ uid_t st_uid; /*用户ID*/ gid_t st_gid; /*组ID*/ dev_t st_rdev; /*设备类型*/ off_t st_pff; /*文件字节数*/ unsigned long st_blksize; /*块大小*/ unsigned long st_blocks; /*块数*/ time_t st_atime; /*最后一次访问时间*/ time_t st_mtime; /*最后一次修改时间*/ time_t st_ctime; /*最后一次属性改变时间*/ } 可以通过系统调用访问stat结构来获取索引节点的相关信息。 stat()系统调用: 所需文件头:#include #include 系统调用功能:stat()函数用来取得文件的属性; 函数原型:int stat(const char *file_name, struct stat *buf ); 函数传入值:将参数file_name 所指的文件状态复制到参数部分buf所致的结构中 返回值:成功返回0,失败返回-1,错误代码存于errno. 和其在一起的还有 int fstat(int f i l e d e s,struct stat * b u f) ; int lstat(const char * p a t h n a m e, struct stat * b u f) ; 这两个函数。 使用s t a t函数最多的可能是ls -l命令,用其可以获得有关一个文件的所有信息。 下表是在< s y s / s t a t . h >中的文件类型宏 想一下,我们应该怎么去使用这些宏定义呢? 下面我们就可以用这些东西来写一判断文件类型的程序了。 #include int main(int argc,char **argv) { int i; struct stat buf; char *ptr; for(i = 1; i < argc; i++) { printf(\"%s: \ if(lstat(argv[i],&buf) < 0) { printf(\"lstat error\\n\"); continue; } if (S_ISREG(buf.st_mode)) ptr = \"regular\"; else if(S_ISDIR(buf.st_mode)) ptr = \"directory\"; else if(S_ISCHR(buf.st_mode)) ptr = \"character special\"; else if(S_ISBLK(buf.st_mode)) ptr = \"block special\"; else if(S_ISFIFO(buf.st_mode)) ptr = \"fifo\"; else if(S_ISLNK(buf.st_mode)) ptr = \"symbolic link\"; else if(S_ISSOCK(buf.st_mode)) ptr = \"socket\"; else ptr = \"unknow mode\"; printf(\"%s\\n\ } exit(0); } 3.数据 通常文件中都包含一定的数据,普通文件和目录文件都有相应的硬盘区存储数据,这些数据是存储在由索引节点指定的位置上,但是一些特殊文件,如设备文件,本身就是一个设备,就不具有这样的在硬盘上的存储区域! 2.2 系统调用 在linux下,程序的功能是使用系统调用来实现的! 在一般情况下,为了更好的保护内核,用户空间的进程是不能访问内核空间的,也就是说进程是不能够存取系统内核的,它不能存取内核使用的内存段,也不能调用内核函数!CPU的硬件结构也保证了这一点。——(见注解) 注:386及以上的CPU实现了4个特权级模式,其中特权级0(Ring0)是留给操作系统代码,设备驱动程序代码使用的,它们工作于系统核心态;而特权极3则给普通的用户程序使用,它们工作在用户态。运行于处理器核心态的代码不受任何的限制,可以自由地访问任何有效地址,进行直接端口访问。而运行于用户态的代码则要受到处理器的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中I/O许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。 但是,万事没有绝对!注意本节开头的一句话,就是在“一般的情况下”,其实,系统存在一个“非一般情况”。这个非一般情况就是:系统调用。 在系统中,真正被进程所使用的内核通信方式叫做系统调用,也就是说,在进程里调用内核中用于实现各种功能的内核函数的通信方式就叫系统调用。系统调用我们也可以理解为一个程序,而这个程序就是Linux内核中设置的一组用于实现各种系统功能的子程序。 根据CPU的硬件结构,进程是不能够直接访问内核中的这些用于实现功能的子函数的。注意,是不能直接。那么通过某种方法是不是就可以访问内核的函数呢?答案是肯定的,这个方法就是系统调用。 系统调用的实现原理简要: 利用中断0x80 中断是指当主机接到外界硬件发来的信号时,马上停止原来的工作,转去处理这一中断事件,在处理完中断事件以后,再回到原来的工作继续工作。而处理处理中断的过程,就是当设备发出了一个中断请求时,CPU停止正在执行的指令,转而跳到包括中断处理代码或者包括指向中断处理代码的 转移指定 所在的内存区域。 使用系统调用的进程先用适当的值填充寄存器,然后调用一个特殊的指令,(这个指令就是中断指令)这个指令会跳到一个事先定义的内核中的一个位置。硬件知道一旦你跳到这个位置,你就不是在限制模式下运行的用户,而是作为操作系统的内核—于是你就可以为所欲为。 进程可以跳转到的内核中的位置叫做system_call。在此位置的过程将检查系统调用号,它将告诉内核进程请求的服务是什么。然后,它再查找系统调用表sys_call_table,找到需要调用的内核函数的地址,并调用此函数,最后返回。 linux里面的每个系统调用是靠一些宏,一张系统调用表,一个系统调用入口来完成的。 2.2.1 宏 宏就是_syscallN(type,name,x...),N是系统调用所需的参数数目,type是返回类型,name即面向用户的系统调用函数名,x...是调用参数,个数即为N。 例如: #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) type name(type1 arg1,type2 arg2,type3 arg3) { long __res; //定义保存结果的变量 __asm__ volatile (\"int $0x80\" //调用中断 : \"=a\" (__res) //把变量保存给通用寄存器eax //将函数名,参数付给其他通用寄存器 : \"0\" (__NR_##name),\"b\" ((long)(arg1)),\"c\" ((long)(arg2)), \"d\" ((long)(arg3))); if (__res>=0) return (type) __res; errno=-__res; return -1; } 这些宏内核源代码中,每一个系统调用和它所对应的系统调用号定义于 usr\\include\\asm\\Unistd.h,这就是为什么你在程序中要包含这个头文件的原因。该文件中还以__NR_name的形式定义了164个常数,这些常数就是系统调用函数name的函数指针在系统调用表中的偏移量。如图2-2.2: 图2-2.2 部分system call 号码 2.2.2 系统调用表 系统调用表定义于内核的源代码中(arch/i386/kernel/entry.S)的最后。 这个表按系统调用号(即前面提到的__NR_name)排列了所有系统调用函数的指针,以供系统调用入口函数查找。 系统调用表sys_call_table (arch/i386/kernel/entry.S)形如: ENTRY(sys_call_table) .long SYMBOL_NAME(sys_setup) /* 0 */ .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) „ „ .long SYMBOL_NAME(sys_stime) /* 25 */ .long SYMBOL_NAME(sys_ptrace) „ „ sys_call_table记录了各sys_name函数(共166项,其中2项无效)在表中的位置。有了这张表,很容易根据特定系统调用在表中的偏移量,找到对应的系统调用响应函数的入口地址。 例2-1:time系统调用 _syscall1(time_t,time,time_t *,tloc) 展开后的情形是这样: time_t time(time_t * tloc) { long __res; __asm__ volatile(\"int $0x80\" : \"=a\" (__res) : \"0\" (13),\"b\" ((long)(tloc))); do { if ((unsigned long)(__res) >= (unsigned long)(-125)) { errno = -(__res); __res = -1; } return (time_t) (__res); } while (0) ; } 可以看出,_syscall1(time_t,time,time_t *,tloc)展开成一个名为time的函数,原参数time_t就是函数的返回类型,原参数time_t *和tloc分别构成新函数的参数。事实上,程序中用到的time函数的原型就是它。 2.2.3 系统调用入口函数 系统调用入口函数定义于entry.s: ENTRY(system_call) pushl %eax # 把eax入栈 SAVE_ALL #ifdef __SMP__ ENTER_KERNEL #endif movl $-ENOSYS,EAX(%esp) cmpl $(NR_syscalls),%eax jae ret_from_sys_call movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax testl %eax,%eax je ret_from_sys_call #ifdef __SMP__ GET_PROCESSOR_OFFSET(%edx) movl SYMBOL_NAME(current_set)(,%edx),%ebx #else movl SYMBOL_NAME(current_set),%ebx #endif andl $~CF_MASK,EFLAGS(%esp) movl %db6,%edx movl %edx,dbgreg6(%ebx) testb $0x20,flags(%ebx) jne 1f call *%eax movl %eax,EAX(%esp) jmp ret_from_sys_call 这段代码现保存所有的寄存器值,然后检查调用号(__NR_name)是否合 法(在系统调用表中查找),找到正确的函数指针后,就调用该函数(即你真正希望内核帮你运行的函数)。运行返回后,将调用ret_from_sys_call。 当在程序代码中用到系统调用时,编译器会将上面提到的宏展开,展开后的代码实际上是将系统调用号放入eax后移用int 0x80使处理器转向系统调用入口,然后查找系统调用表,进而由内核调用真正的功能函数。 2.2.4 系统调用实际的应用 Linux下的每一个程序其真正的实现都是通过系统调用实现的!(老天,它看起来是那么复杂,内嵌汇编语言是不是听上去很牛。)那么我们程序该如何使用系统调用呢?其实很简单,内核给我们提供了API,让我们可以向调用普通函数那样调用系统调用。唯一的区别就是系统调用由操作系统核心提供,运行于核心态;而普通的函数调用由函数库或用户自己提供,运行于用户态。 附录A:常见Linux系统调用表 2.3 不带缓存的I/O 2.3.1 文件描述符: 在Linux环境下,内核如何区分和引用的特定文件呢?这里面就用到一个重要概念,文件描述符,对于Linux而言,所有的设备和文件的操作都是使用文件描述符来进行的,文件描述符是一个非负的整数,它是一个索引的值,并指向内核中每个进程打开文件的记录表,当打开一个现存或创建一个文件时,内核就返回一个文件描述符,当需要读写文件时,就需要把文件描述符作为参数传递给相应的函数。 通常,一个进程启动时,都会打开3个文件:标准输出、标准输入,标准出错处理,这3个文件分别对应的文件描述为0,1,2 (相对于他们的宏替换是:STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO) 2.3.2 系统调用文件操作: 这一节,我们来详细学习一下系统调用文件操作,也就是不带缓存的文件操作,这里说的不带缓存是指每一个函数都只调用系统中的一个函数,所以有时我们也称他为系统调用文件操作。 在我们Linux应用开发中,有着著名的五大系统调用:这五大系统调用是我们学习Linux应用程序开发的根基。他们是open,close ,read,write 和lseek!这些不带缓存的I / O函数不是ANSI C的组成部分,但是是P O S I X . 1和X P G 3的组成部分。 (1) open和close 系统调用(函数)说明 open函数是用来打开或创建文件的,同时,在打开或创建文件时可以指定文件的属性及文件的权限等各种参数。 Close函数是用于关闭一个打开的文件,当一个进程终止时,它所有已打开的文件都由内核自动关闭。 open函数的语法格式如表2-3-2所示: 所需文件头: #include Flag参数说明 参数flag O_RDONLY O_WRONLY O_RDWR O_CREAT O_EXCL O_NOCTTY O_TRUNC O_APPEND Perms参数说明: 被打开文件的存取权限,为8进制表示。 mode宏替换: 参数mode S_IRUSR S_IWUSR S_IXUSR S_IRGRP S_IWGRP S_IXGRP S_IROTH S_IWOTH S_IXOTH Close函数说明: 所需文件头: 函数功能 函数原型 函数传入值: 函数返回值 备注 说明 拥有者具有读权限 拥有者具有写权限 拥有者具有执行权限 组用户具有读权限 组用户具有写权限 组用户具有执行权限 其他用户具有读权限 其他用户具有写权限 其他用户具有执行权限 说明 只读方式打开 可写方式打开 读写方式打开 如果文件不存在,那么创建这个文件,并且用第三个参数为其设置权限 如果文件使用O_CREAT时文件存在,则可返回错误消息,这一参数可用于测试文件是否存在 如果文件为终端,那么终端不可以作为调用open系用调用的那个进程的控制终端 如果文件已存在,并且以只读或只写方式成功打开,那么会先删除文件中原有的数据 以添加方式打开,在打开文件的同时,文件指针指向文件的末尾 #include 例:2-1,要求打开文件/home/2-1file,如果文件不存在就创建此文件,如果文件存在,清空数据后,并且使用可写方式打开,同时设定文件的权限0600. 操作步骤: 1. 编辑源文件2-1 [root@localhost char2]vim 2-1.c 2 . 源程序代码如下: #include #include #include if((fd=open(\"/home/2-1file\ perror(\"打开文件失败\"); else{ printf(\"打开(创建)文件2-1file,文件描述符是:%d\\n\exit(1); if(close(fd)<0){ perror(\"关闭文件时出错\"); exit(1); else{printf(\"文件已经顺利关闭\\n\"); } system(\"ls /home/2-1file -l\"); return 0; } #include #include #include int fd; if((fd=open(\"/home/2-1file\ perror(\"打开文件失败\"); } } } } else{ exit(1); printf(\"打开(创建)文件2-1file,文件描述符是:%d\\n\if(close(fd)<0){ perror(\"关闭文件时出错\"); exit(1); else{printf(\"文件已经顺利关闭\\n\"); } system(\"ls /home/2-1file -l\"); return 0; 3.编译程序: [root@localhost char2]gcc –o 2-1 2-1.c 编译成功执行程序如下图所示: (2) read ,write和lseek系统调用: read函数用于将指定的文件描述符(文件)中读出数据。 write函数用于向打开的文件中写数据。 写操作从文件当前位移量处开始,若磁盘已满或超出该文件的长度,则write函数返回失败。 lseek 函数用于在指定的文件描述符中将指针定位带相应的位置。 read函数语法: 所需文件头: 函数功能 函数原型 函数传入值: #include write函数说明 所需文件头: 函数功能 函数原型 函数传入值: #include lseek函数说明 所需文件头: 函数功能 函数原型 函数传入值: #include 成功:返回写入的字节数。 有错误发生返回-1。 例2-3打开/etc/passwd用read 函数读取文件内容,建立新文件,并使用write把读到的内容写入新文件 操作步骤: 1. 编辑源程序: [root@localhost char2]vim 2-3.c 程序源代码如下: #include #include #include int fdsrc, fddes,nbytes; int flags=O_CREAT|O_TRUNC|O_WRONLY; int z; char buf[256],des[256]; printf(\"请输入目标文件名:\"); scanf(\"%s\ fdsrc=open(\"/etc/passwd\ if(fdsrc<0){ exit(1); fddes=open(des,flags,0644); if(fddes<0){ exit(1); while((nbytes=read(fdsrc,buf,256))>0){ z=write(fddes,buf,nbytes); if(z<0){ perror(\"写目标文件出错\"); close(fdsrc); close(fddes); printf(\"复制/etc/passwd文件为%s文件成功!\\n\exit(0); 2.4文件上锁 前面的5个系统调用基本实现了对文件的I/O操作,但是,我们知道Linux是多任务系统,那么当一个共享文件被多个用户共同使用的时候,该如何避免共享时所发生的资源竞争呢? 解决之道是给文件上锁! 文件上锁包括建议性上锁和强制性上锁,建议性上锁要求每个上锁的的进程都要检查是否有锁的存在,并且尊重已有的锁,内核和系统都不使用建议性上锁,强制性锁是由内核执行的锁. 在Linux中,实现上锁的系统调用有lock 和 fcntl ,lock用于对文件进行建议性上锁,fcntl不仅可以进行建议性上锁还可以施加强制性上锁。同时,fcntl 还可以对文件的某一记录进行上锁,也就是记录锁。 记录锁又可以分为读取锁和写入锁,读取锁又称共享锁,他能够使用多个进程都能在文件的同一部分建立读取锁,而写入锁又称排斥锁,任何一个时候只能有一个进程在文件的某个部分建立写入锁。 当然,在同一文件的同一位置,不能同时建立读取锁和写入锁。 fcntl系统调用不仅仅用于文件上锁,(虽然这是它最重要的功能),它还可以查看或设置文件的一些相关信息。 我们先来看一下lock的结构体: Struct flock{ Short l_type; //锁类型 Off_t l_start; //相对位移量 Short l_whence; //相对位移量的起点 Off_t l_len; // 加锁区的长度 Pid_t l_pid; //进程ID } 锁类型与位移量的起点的取值: l_type F_RDLCK 读取锁(共享锁) F_WRLCK 写入锁(排斥锁) F_UNLCK 解锁 l_whence SEEK_SET 当前位置为文件的开头,新位置为偏移量的大小 SEEK_CUR 当前位置为文件指针的位置,新位置为当前位置加上偏移量的大小 SEEK_END 当前位置为文件的末尾,新位置为当前位置加上偏移量的大小 fcntl系统调用语法: 所需文件头: #include 步骤1:编辑源文件: [root@localhost char2]vim 2-4.c 程序源代码如下: #include struct flock lock; lock.l_whence = SEEK_SET; lock.l_start = 0; lock.l_len = 0; while(1){ lock.l_type = type; if((fcntl(fd,F_SETLK,&lock))==0){/*根据不同的TYPE给文件上不同的锁或解锁*/ if(lock.l_type==F_RDLCK) printf(\"进程%d给文件加的锁是读取锁\\n\else if(lock.l_type == F_WRLCK) printf(\"进程%d给文件加的是写入锁\\n\else if(lock.l_type == F_UNLCK) printf(\"释放强制解锁:%d\\n\return; } } } } int main(){ } lock_set(fd,F_WRLCK); getchar(); lock_set(fd,F_UNLCK); getchar(); close(fd); int fd; fd=open(\"/home/2-4file\T,0666); if(fd<0){ perror(\"打开出错\"); exit(1); fcntl(fd,F_GETLK,&lock); /*判断文件是否可以上锁*/ if(lock.l_type != F_UNLCK){ if(lock.l_type==F_RDLCK) printf(\"进程%d给文件已经被上读取锁\\n\ if(lock.l_type==F_WRLCK) printf(\"进程%d给文件已经被上写入锁\\n\getchar(); exit(0); } 2.5 特殊文件操作 在linux系统中,所谓特殊文件,就是指普通文件以外的其他文件,除了普通文件外,还有其他三类文件:设备文件,目录文件,链接文件,管道文件!在这一节里,我们将学习以上所述的,区别于普通文件的操作——特殊文件操作。 2.5.1 目录文件操作: 目录文件是linux中一种比较特殊的文件,它是linux文件系统结构的骨架,对构成整个树形层次结构的linux文件系统非常重要。 与文件目录相关的mkdir,opendir,closedir ,readir,scandir(系统调用)等: mkdir函数说明: 所需头文件 函数功能: 函数原型: 函数传入值: 函数返回 #include opendir函数说明: 所需头文件 函数功能: 函数原型: 函数传入值: #include readdir函数说明: 所需头文件 #include closedir函数说明: 所需头文件 函数功能: 函数原型: 函数传入值: 函数返回 #include 例 2-6 设计一个程序,要求创建一个目录,并读取系统目录文件“/etc/rc.d”中所有的目录结构! 操作步骤: 步骤1:编辑源文件 [root@localhost char2]vim 2-6.c 程序源代码如下: #include system(\"ls /root\"); dir1=opendir(\"/etc/rc.d\"); while((ptr = readdir(dir1))!=NULL) { printf(\"目录:%s\\n\ } printf(\"位移:%d\\n\printf(\"长度:%d\\n\printf(\"索引节点号:%d\\n\printf(\"\\n\"); closedir(dir1); rmdir(\"/root/test\"); printf(\"删除目录成功!\\n\"); 步骤2:编译并执行程序后部分结果如下图: 2.5.2 衔接文件操作 衔接文件有点像windows系统中的快捷方式,但是又不完全一样。Linux系统中的链接有两种方式:软链接和硬链接。 1. 软链接文件 软连接又叫符号链接,这个文件包含了另一个文件的路径名,可以是任意文件或目录,可以链接不同文件系统的文件的路径名。 symlink系统调用说明: 所需头文件 函数功能: #include 步骤1 :编译源文件: [root@localhost char2]vim 2-7.c 程序源代码如下: #include 2.硬链接文件: 硬链接文件大体上和软连接执行的效果差不多,但是硬链接不允许给目录建立链接并且不能跨系统。 link系统调用, 所需头文件 函数功能: 函数原型: 函数传入值: #include 函数返回 成功返回0,失败返回-1. 备注: 例 2-8,设计一个程序,要求为系统中某一文件建立硬链接,并查看其属性! 操作步骤: 步骤一:编译源文件: [root@localhost char2]vim 2-7.c 程序源代码如下: #include printf(―链接成功!‖); system(―ls 2-8link -l‖); system(―ls 2-1.c -l‖); } return 0; } 步骤2:编译并执行程序结果如下: 软链接和硬链接的区别,总的来说可以归纳为4点: (1)软连接可以 跨文件系统 ,硬连接不可以 。实践的方法就是用共享文件把windows下的 aa.txt文本文档连接到linux下/root目录 下 bb,cc . ln -s aa.txt /root/bb 连接成功 。ln aa.txt /root/bb 失败 。 (2)关于索引节点的问题 。硬连接不管有多少个,都指向的是同一个索引节点,会把 结点连接数增加 ,只要结点的连接数不是 0,文件就一直存在 ,不管你删除的是源文件还是 连接的文件 。只要有一个存在 ,文件就 存在 (其实也不分什么 源文件连接文件的 ,因为他们指向都是同一个 I节点)。 当你修改源文件或者连接文件任何一个的时候 ,其他的 文件都会做同步的修改 。软链接不直接使用索引节点号作为文件指针,而是使用文件路径名作为指针。所 以 删除连接文件 对源文件无影响,但是 删除 源文件,连接文件就会找不到要指向的文件 。软链接有自己的inode,并在磁盘上有一小片空间存放路径名. (3)软连接可以对一个不存在的文件名进行连接 ,硬链接不行。 (4)软连接可以对目录进行连接,硬链接不行。 umask函数 u m a s k函数为进程设置文件方式创建屏蔽字,并返回以前的值。(这是少数几个没有出错返回的函数中的一个。) 其中,参数c m a s k由上面所说的9个常数( S _ I R U S R , S _ I W U S R等)逐位“或”构成的。在进程创建一个新文件或新目录时,就一定会使用文件方式创建屏蔽字。 #include 这两个函数使我们可以更改现存文件的存取许可权。 c h m o d函数在指定的文件上进行操作,而f c h m o d函数则对已打开的文件进行操作。为了改变一个文件的许可权位,进程的有效用户I D必须等于文件的所有者,或者该进程必须具有超级用户许可权。 参数m o d e是下表中所示常数的某种逐位或运算。 注意,两个设置- I D常数( S _ I S〔U G〕ID), 保存-正文常数( S _ I S V T X ),以及三个组合常数( S _ I RW X〔U G O〕)。(这里使用了标准U N I X字符类算符〔〕,表示方括号算符中的任何一个字符。例如,最后一个,S _ I RW X〔U G O〕表示了三个常数: S _ I RW X U、S _ I RW X G和S _ I RW X O。这一字符类算符是大多数UNIX shell和很多标准U N I X应用程序都提供的正规表达式的一种形式。) #include c h o w n函数可用于更改文件的用户I D和组I D。 除了所引用的文件是符号连接以外,这三个函数的操作相类似。在符号连接情况下, l c h o w n更改符号连接本身的所有者,而不是该符号连接所指向的文件。 6. 文件的时间 对每个文件保持有三个时间字段,它们的意义示于下表中。 注意修改时间( s t _ m t i m e )和更改状态时间( s t _ c t i m e )之间的区别。修改时间是文件内容最后一次被修改的时间。更改状态时间是该文件的i节点最后一次被修改的时间。在本章中我们已说明了很多操作,它们影响到i节点,但并没有更改文件的实际内容:文件的存取许可权、用户I D、连接数等等。因为i节点中的所有信息都是与文件的实际内容分开存放的,所以,除了文件数 据修改时间以外,还需要更改状态时间。 2.5.4 文件时间相关函数 7. utime函数 一个文件的存取和修改时间可以用u t i m e函数更改。 此函数所使用的结构是: struct utimbuf { time_t actime; /*access time*/ time_t modtime; /*modification time*/ } 此结构中的两个时间值是日历时间。这是自1 9 7 0年1月1日,0 0 : 0 0 : 0 0以来国际标准时间所经过的秒数。 此函数的操作以及执行它所要求的优先权取决于t i m e s参数是否是N U L L。 (1) 如果t i m e s是一个空指针,则存取时间和修改时间两者都设置为当前时间。为了执行此操作必须满足下列两条件之一: ( a )进程的有效用户I D必须等于该文件的所有者I D,( b )进程对该文件必须具有写许可权。 (2) 如果t i m e s是非空指针,则存取时间和修改时间被设置为t i m e s所指向的结构中的值。此时,进程的有效用户I D必须等于该文件的所有者I D,或者进程必须是一个超级用户进程。对文件只具有写许可权是不够的。 注意,我们不能对更改状态时间s t _ c t i m e指定一个值,当调用u t i m e函数时,此字段被自动更新。 在某些U N I X版本中,t o u c h ( 1 )命令使用此函数。另外,标准归档程序t a r ( 1 )和c p i o ( 1 )可选地调用u t i m e ,以便将一个文件的时间值设置为将它归档时的值。 #include #include \"../header/ourhdr.h\" int main(int argc, char *argv[]) { int i; struct stat statbuf; struct utimbuf timebuf; for (i = 1; i < argc; i++) { if (stat(argv[i], &statbuf) < 0) { printf(\"%s: stat error\ continue; } if (open(argv[i], O_RDWR | O_TRUNC) < 0) { printf (\"%s: open error\ continue; } timebuf.actime = statbuf.st_atime; timebuf.modtime = statbuf.st_mtime; if (utime(argv[i], &timebuf) < 0) { printf (\"%s: utime error\ continue; } } exit(0); } 8. chdir, fchdir 和getcwd 函数 每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径名)。当用户登录到U N I X系统时,其当前工作目录通常是口令文件( / e t c / p a s s w d )中该用户登录项的第6个字段—用户的起始目录。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。进程调用c h d i r或f c h d i r函数可以更改当前工作目录。 在这两个函数中,可以分别用p a t h n a m e或打开文件描述符来指定新的当前工作目录。 #include \"../header/ourhdr.h\" int main(void) { if (chdir(\"/tmp\") < 0) printf (\"chdir failed\"); printf(\"chdir to /tmp succeeded\\n\"); exit(0); } 我们需要一个函数,它从当前工作目录开始,找到其上一级的目录,然后读其目录项,直到该目录项中的i节点编号数与工作目录i节点编号数相同,这样地就找到了其对应的文件。按照这种方法,逐层上移,直到遇到根,这样就得到了当前工作目录的绝对路径名。很幸运,函数g e t c w d就是提供这种功能的。 向此函数传递两个参数,一个是缓存地址b u f,另一个是缓存的长度s i z e。该缓存必须有足够的长度以容纳绝对路径名再加上一个n u l l终止字符,否则返回出错。 #include \"../header/ourhdr.h\" int main(void) { char *ptr; int size; if (chdir(\"/usr/spool/uucppublic\") < 0) printf(\"chdir failed\"); ptr = path_alloc(&size); /* our own function */ if (getcwd(ptr, size) == NULL) printf (\"getcwd failed\"); printf(\"cwd = %s\\n\ exit(0); } 第三章 串行通信 3.1 串行通信概述 串行通信(Serial Communication),就是通过串口实现通信,它一占据着其重要的地位,随着计算机性能的提高,其应用范围也使用的越来越广泛。 串口叫做串行接口,也称串行通信接口,按电气标准及协议来分包括RS-232-C、RS-422、RS485,usb等. 如图: RS-232-C:也称标准串口,是目前最常用的一种串行通讯接口。它是在1970年由美国电子工业协会(EIA)联合贝尔系统、 调制解调器厂家及计算机终端生产厂家共同制定的用于串行通讯的标准。它的全名是―数据终端设备(DTE)和数据通讯设备(DCE)之间 串行二进制数据交换接口技术标准‖。传统的RS-232-C接口标准有22根线,采用标准25芯D型插头座。后来的PC上使用简化了的9芯D型插座。现在应用中25芯插头座已很少采用。现在的电脑一般有两个串行口:COM1和COM2,你到计算机后面能看到9针D形接口就是了。如图:现在有很多手机数据线或者物流接收器都采用COM口与计算机相连。 RS-422:为改进RS-232通信距离短、速率低的缺点,RS-422定义了一种平衡通信接口,将传输速率提高到10Mb/s,传输距离延长到4000英尺(速率低于100kb/s时),并允许在一条平衡总线上连接最多10个接收器。RS-422是一种单机发送、多机接收的单向、平衡传输规范,被命名为TIA/EIA-422-A标准。 RS-485:为扩展应用范围,EIA又于1983年在RS-422基础上制定了RS-485 标准,增加了多点、双向通信能力,即允许多个发送器连接到同一条总线上,同时增加了发送器的驱动能力和冲突保护特性,扩展了总线共模范围,后命名为TIA/EIA-485-A标准。 Universal Serial Bus(通用串行总线)简称USB,是目前电脑上应用较广泛的接口规范,由Intel、Microsoft、Compaq、IBM、NEC、Northern Telcom等几家大厂商发起的新型外设接口标准。USB接口是电脑主板上的一种四针接口,其中中间两个针传输数据,两边两个针给外设供电。USB接口速度快、连接简 单、不需要外接电源,传输速度12Mbps,最新USB2.0可达480Mbps;电缆最大长度5米,USB电缆有4条线,2条信号线,2条电源线,可提供5伏特电源,USB电缆还分屏蔽和非屏蔽两种,屏蔽电缆传输速度可达12Mbps,价格较贵,非屏蔽电缆速度为1.5Mbps,但价格便宜; 3.2 串口的通信格式 串口形容一下就是像一条车道,串行通信是利用一条传输线将资料一位位的顺序传送,特点是通信线路简单,利用简单的线缆就可以实现通信,降低成本,适用于远距离通信,但传输速度慢的应用场合。 波特率(BaudRate),模拟线路信号的速率,也称调制速率,以波形每秒的振荡数来衡量。 流控方法流控(也叫联络)是用于通信设备之间管理数据流的异步通信协议。如果一台工作站在一个时刻接收的信息多于它的缓存或处理的能力,它就向发送方发出信号请求暂停传送直到它跟得上发送方。 在Linux中,所有的设备都在“/dev”下,其中,COM1,COM2,对应的设备名依次为“/dev/ttyS0”,“/dev/ttyS1”,Linux对设备的操作方法和对文件的操作方法相同,因此对串口的读写,我们可以使用简单的“read”,“write”函数来完成,所不同的是,我们需要对串口的一些参数进行一些设置。 思考,为什么要对串口进行设置? 3.3 串行通信程序的设计 串行通信的程序编程的基本流程如下: 1, 2, 3, 4, 打开串口; 保存原端口的设置(为了安全和以后调试程序的需要); 根据用户的实际需要,配置相应的波特率,字符大小,奇偶校验位,停止位等参数; 通信的读写操作; 5, 通信结束后,关闭串口。 我们直接以一个实例来具体讲解并学习关于串口通信的编程! 实验:实现串口通信:没有开发板的情况下,我们使用PC机的主盘来进行试验。普通PC机上有个串口:COM1,COM2. 那么,我们要实现的是通过计算机的COM1和COM2进行通信,利用RS-232来传递消息,其中COM1为发射端,COM2为接收端,当收到“#“字符时,结束传输,通信格式是:38400,n,8,1 (38400表示波特率,N表示不进行奇偶校验,8表示数据位,1表示停止位)。 操作步骤: 步骤1: 1.>打开串口 int fd; fd = open(“/etc/ttyS0”,”O_RDWR|O_NOCTTY_|O_NONBLOCK”) O_NOCCTY:用于通知Linux系统,这个程序不会成为对应这个端口的控制终端,如果没有这个标志,那么任何一个输入,比如键盘终止信号等,都将会影响你的进程。 O_NONBLOCK:标志说明这个程序以非阻塞的方式打开! 2.>配置串口属性 对串口的属性的设置,就是设置串口的通信格式,也就是我们前面所说的波特率奇偶校验位,数据位,停止符等等。这些属性被封装在一个叫termios的结构体中,这个结构体的声明位于”termios,h”头文件中,如果使用该结构体,则需要包“#include Struct termios { } tcflag_t c_iflag //输入模式 tcflag_t c_oflag //输出模式 tcflag_t c_cflag //控制模式 tcflag_t c_lflag //局部模式 cc_t c_cc[nccs] //特殊控制模式 在这个结构体中,最重要的是 c_cflag,通过对它的赋值,用户可以设置波特率,字符大小,数据位,停止位,奇偶校验位和硬件控制等。 我们先来看一下c_iflag的具体参数(值)的含义及用法: 输入模式c_iflag 它的成员控制端口发送端的字符处理。(CR回车,NL换行) c_iflag的值 BRKINT IONBRK ICRNL IGNCR INLCR IGNPAR INPCK PARMRK ISTRIP IXOFF IXON 说明 响应中断 屏蔽中断 将CR转换成NL 忽略收到的CR 将NL转换成CR 忽略奇偶校验错误 收到的字符进行奇偶校验 表示奇偶校验错误 除去奇偶校验位 设置软件的输入数据流 设置软件的输出数据流 输出模式:c_oflag 它的成员控制接收端的字符处理。(CR回车,NL换行) 打开输出处理 OPOST ONLCP OCRNL ONOCR ONLRET OFILL OFDEL NLDLY CRDLY TABDLY BSDLY 将NL转换成CR和NL 将CR转换成NL 不在第零列输出回车 不输出CR 传送fill字符以提供延迟 使用del作为fill字符,而不是NULL 换行延时 Return键延时 Tab键延时 后退延时 控制模式c_cflag,它的成员用来设置通信格式 CLOCAL CREAD CSn CSTOPB HUPCL PARENB PARODD Bn 忽略任何调制解调器状态 启动接收器 n个数据位n=(5 ; 6 ; 7 ; 8) 2个停止位(不设置则为一个停止位) 关闭时挂断调制解调器 启用奇偶校验 奇校验 N代表波特率 n的选择有: 0 19200 1800 38400 2400 57600 4800 115200 9600 CSIZE 数据位的掩码 c_lflag局部控制模式 它的成员局部属性进行控制: ECHO ECHOE ECHOK ECHONL ICANON IEXTEN NOFLSH TOSTOP 启动相应输入字符 将ERASE字符响应为执行Backspace.Space,Backpase 将KILL字符处删除当前行 回显字符NL 设置正规模式,在这种模式下,需要设置C_CC数组来进行一些终端配置。 启动特殊函数执行 关闭队列中的FLUSH 传送要写入的信息至背景程序 c_cc 特殊控制模式它的成员用于通信的相关其他的控制。 VINIP VQUIT VERASE VKILL VEOF VEOL VMIN VTIME 中断控制 退出操作 删除操作 删除行 位于文件结尾 位于文件行尾 指定最少读取的字符 指定了读取每个字符的等待时间 接下来,我们来看看具体是如何配置的: 1. 为了安全起见和以后调试程序方便,可以先保存原先串口的配置,使用函数tcgetattr(fd,&oldtio),函数得到与FD指向对象的相关参数,并将它们保存于oldtio引用的termios结构中,此函数还可以测试配置是否正确,该串口是否可用。成功返回0,失败返回-1,如下所示: struct termios oldtio ,newtio; if (tcgetattr(fd,&oldtio)!=0){ perror(―setup Serial 1‖); } return -1; 2,设置控制模式(打开本地连接,并忽略调制解调器状态,开启接收) newtio.c_cflag |=CLOCAL | CREAD; 3,设置波特率: 设置波特率有专门的函数,用户不能通过位掩码来操作,设置波特率的主要函数有: cfsetispeed (&newtio, B115200); cfsetospeed (&newtio, B115200); 一般的,用户将输出输入函数的波特率设置成一样,这个函数在成功时返回 0,失败时返回-1. 4.设置字符大小 与设置波特率不同,设置字符大小没有现成的函数,需要用位掩码,一般首先去除数据位中的位掩码,在重新按要求设置。 newtio.c_flag & = ~CSIZE;(先按位取反,然后置0); newtio.c_flag |= CS8; 5.设置奇偶校验位 设置奇偶校验位需要用到两个termios中的成员,c_cflag,和 成c_iflag;首先要激活c_cflag中的校验参数PARENB和判断是否要进行偶校验,同时在激活c_iflag中的奇偶校验; 如果使用奇校验: newtio.c_cflag |= PARENB ;(启用奇偶校验) newtio.c_cflag |= PAROOD ;(启用奇校验) &=~PAROOD(使用偶校验); newtio.c_iflag |= (INPCK | ISTRIP); 6.设置停止位 newtio.c_iflag &= ~CSTOPB; 7,处理要写入的引用对象 函数teflush(fd , queue_selector) 可以用来处理要写入的引用对象,其方法取决于queue_selector的取值: TCIFLUSH:刷新收到的数据但是不读;(清空收到的数据) TCOFLUSH:刷新写入的数据但是不传送;(清空写入的数据) TCIOFLUSH:同时刷新收到的数据但是不读,刷新写入的数据但是不传送; 8,激活设置 上面所有的设置完成配置之后,要激活刚才的设置并使配置生效,使用函数tcsetattr(fd,OPTION,&newtio); newtio是刚刚设置好的termios结构的变量,OPTION可取的值有三种, TCSANOW : 改变的配置立即生效; TCSADRAIN : 改变的配置在有写入fd 的输出都结束后生效; TCSAFLUSH : 改变的配置在多有写入FD引用对象的输出都被结束后生效,所有已接受但为读入的输出都在改变发生前丢弃。 函数调用成功返回0,失败 -1, 比如: if ( ( tcsetattr(fd , TCSANOW, &newtio))!=0 ) { perror (―com set error‖); return -1; } 完整的串口配置如下: int set_opt(int fd, int nspeed,int nBits,char nevent ,int nStop){ struct termios newtio,oldtio; if (tcgetattr(fd,&oldtio)!=0){ perror(―setup Serial 1‖); return -1; } bzero(&newtio , sizeof(newtio)); /* 激活选项并设置字符大小*/ newtio.c_flag |=CLOCAL | CREAD; newtio.c_flag & = ~CSIZE; switch (nBits) { case 7 : newtio.c_flag |= CS7; break; case 8 : newtio.c_flag |= CS8; break; } /* 设置奇偶校验位*/ Switch (nEvent){ case ‗O‘ : //奇数 newtio.c_cflag |= PARENB ; newtio.c_cflag |= PAROOD ; newtio.c_iflag |= (IKPCK | ISTRIP); break; case ‗E‘ : //偶数 newtio.c_cflag |= PARENB ; newtio.c_cflag &= ~PAROOD ; newtio.c_iflag |= (IKPCK | ISTRIP); break; } case ‗N‘: newtio.c_cflag &= ~PARENB ; break; Switch (nSpeed) { case 2400: cfsetispeed(&newtio,B2400); cfsetospeed (&newtio, B2400); break; case 4800: cfsetispeed(&newtio,B4800); cfsetospeed (&newtio, B4800); break; case 9600: case 460800: cfsetispeed(&newtio,B460800); cfsetospeed (&newtio, B46000); break; case 38400: cfsetispeed(&newtio,B38400); cfsetospeed (&newtio, B38400); break; cfsetispeed(&newtio,B9600); cfsetospeed (&newtio, B9600); break; default: cfsetispeed(&newtio , B9600); cfsetispeed(&newtio , B9600); break; } /*设置停止位*/ If ( nStop == 1 ) newtio.c_cflag &= ~CSTOPB; else if(nStop == 2 ) newtio.c_cflag |= CSTOPB; /*设置等待时间和最小接受字符*/ newtio.c_cc[VTIME] = 0 ; newtio.c_cc[VMIN] = 0 ; /*清空字符*/ tcflush(fd,TCIFLUSH) ; /*激活新配置*/ if ((tcsetattr(fd,TCSANOW,&newtio))!= 0){ perror (―com set error‖); return -1; } print (―set done!\\n‖); return 0 ; } 了解了整个配置过程后,我们来完成我们的实验: 发送端 COM1,他会把数据发送给COM2,若COM2接受的字符为“#”,则结束传输。 发送端: #include #include #define MODEMDVICE ―/dev/ttyS0‖ #define STOP ‗#‘ int main(){ int fd , pz, c= 0 , res; struct termios oldtio,newtio; char ch ,s1[2]; printf(―start„„\\n‖); fd =open(MODEMDVICE,O_RDWR | O_NOCTTY); if(fd<0){ perror(MODEMDVICE); exit(1); } printf(―open„„\\n‖); int pz = set_opt(fd, 38400,8,N,1); if(pz==0){ } } printf(―writing!!!!\\n‖); while(1){ while((ch=getchar())!=‘#‘){ s1[0]=ch; res =write(fd,s1,1); s1[0]=ch; s1[1]=‘\\n‘; res=write(fd,s1,2); break; printf(―close„„\\n‖); close(fd); tcsetattr(fd,TCSANOW,&oldtio); return 0; }else { close(fd); perror(―串口设置失败‖); exit(1;) return -1; } } 接收端: #include #define MODEMDVICE ―/dev/ttyS1‖ #define STOP ‗#‘ int main(){ int fd , pz, c= 0 , res; struct termios oldtio,newtio; char buf[256]; print(―start„„\\n‖); fd =open(MODEMDVICE,O_RDWR | O_NOCTTY); if(fd<0){ perror(MODEMDVICE); exit(1); } printf(―open„„\\n‖); int pz = set_opt(fd, 38400,8,N,1); if(pz==0){ printf(―reading!!!!\\n‖); while(1){ res = read(fd,buf,255); buf[res]=0; printf(―res=%d vuf=%s\\n‖,res,buf); if(buf[0]=‘#‘) break; } printf(―close„„\\n‖); close(fd); tcsetattr(fd,TCSANOW,&oldtio); return 0; }else { close(fd); perror(―串口设置失败‖); exit(1;) return -1; } } 第四章 进程控制 4.1 linux进程概述 进程简单的说就是一个程序一次执行的过程,它是一个动态的概念。按照教科书上的定义,进程是程序执行的实例,是linux的基本调度单位。 对于程序员来说,最重要的就是要区分进程和程序的区别,程序是指一段完成功能的代码,或者说是一个工具,它是一个静态的概念,而进程,它是动态的,比如,linux的vi编辑器,它就是一段在linux下用于文本编辑的工具,那么它是一个程序,而我们在linux终端中,可以分别开启两个vi编辑器的进程。一旦提到进程,我们的脑子里就应该产生——程序从代码的第一句动态的执行到最后一句这样的一个思路。 一个进程由如下元素组成: 1.> 进程的当前上下文,即进程的当前执行状态; 2.> 进程的当前执行目录 3.> 进程访问的文件和目录 4.> 进程的访问权限,比如它的文件模式和所有权 5.> 内存和其他分配给进程的系统资源 在linux系统中,内核使用进程来控制对CPU和其他系统资源的访问,并且使用进程来决定在CPU上运行哪个程序,运行多久以及采用什么特性运行它。内核的调度器负责在所有的进程间分配CPU执行时间,称为时间片(time slice),它轮流在每个进程分得的时间片用完后从进程那里抢回控制权。 OS会为每个进程分配一个唯一的整型ID,做为进程的标识号(pid)。进程除了自身的ID外,还有父进程ID(ppid),所有进程的祖先进程是同一个进程,它叫做init进程,ID为1,init进程是内核自检后的一个启动的进程。init进程负责引导系统、启动守护(后台)进程并且运行必要的程序。 简单来说,Linux的进程就相当于Windows系统中的任务,每个在linux下运行的程序都是一个进程,每个进程包含(属于自己唯一的)进程标识符及数据,这些数据包含进程变量,外部变量及进程堆栈等。 Linux下的进程管理: 1. 手工(启动)管理 1> 前台启动:用户直接使用命令启动一个程序,就启动了一个进程 2> 后台启动:用户在使用命令尾后加一个“&”,就表示这个进程在后台运行。 2. 调度(启动)管理 用户先调度安排,制定进程运行的时间和场合的启动方式。 比如:at 17:30 8/8/2008 制定在2008年8月8号下午5点半执行命令,启动进程。 Linux常见进程命令 ps top nice renice crontab kill bg 查看系统中的进程 动态的现实系统中的进程 按用户的指定的优先级运行 改变正在运行进程的优先级 用于安装、删除或者列出用于驱动cron后台进程 终止进程 将挂起的进程放到后台执行 4.2 进程控制 所谓控制,就是将进程的一个完整的生命周期(进程的诞生,繁衍与消亡)完全的控制在在程序员的手中。 刚才提到,在Linux 环境下启动进程时,系统会自动分配一个唯一的数值给这个进程,这个数值就是这个进程的标识符。 在Linux 中主要的进程标识符有进程号(PID),和它的父进程(PPID)(parents process ID)。PID和PPID都是非零的正整数,在linux中获得当前的进程PID和PPID的系统调用函数为getpid和getppid 。 getpid系统调用说明: 所需头文件 函数功能: 函数原型: 函数传入值: 函数返回 #include 在linux下,创建进程的系统调用有三个,它们是: 1.> exec家族函数; 2.> fork函数。 3.> system函数; (1) exec家族函数 首先,为什么说是exec家族函数呢?其实exec并不是完整的函数,而是有一系列都是以exec开头,实现进程创建功能的函数。exec必须包含传入参数的说明后缀,如下所示: exec家族函数后缀说明: 前4位 第五位 第六位 统一为exec l: 参数传递为逐个列举方式 v: 参数传递为构造指针数组方式 e: 可以传递新进程的环境变量 p: 可执行文件查找方式为文件名 execl , execle , execlp execv , execve , execvp , execle , execve , execlp , execvp exec不能单独使用,必须配合第五位或第六位参数(或者第五,六位参数),以便告诉内核参数的传递形式,内核会根据第五,六位的参数来解读参数内容。 具体如下: 所需文件头 函数功能 函数原型 #include 2.>参数的传递方式: 这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身),要注意的是. 这些参数必须以NULL表示结束,如果是列举方式,那么要把它强制转化成一个字符指针。 3.>环境变量: exec函数可以默认系统的环境变量,也可以转入指定的环境变量。由e,p两个参数控制。 其中最常用的是:execve 以参数传递为构造指针数组方式,并且可以传递新进 参数传递方式有两种,l — 参数传递为逐个列举方式,v —参数传递为构造指针数组方式, 在表中,前4个参数的查找方式都是完整的文件目录路径,而最后两个是可以只给出文件名,系统就会到默认的环境变量“$PATH”所指的路径中进行查找。 程的环境变量。我们看下面的例子: 例4-2,创建一个进程,并返回新进程的pid! 操作步骤: 步骤1:编辑源文件: [root@localhost char2]vim 4-1.c 程序源代码如下: #include char *args[]={\"vim\ printf(\"系统分配的进程号是:%d\\n\if(execve(\"/usr/bin/vim\ perror(\"用exec函数创建进程失败\"); return 1; 步骤2:编译并执行程序结果如下: 这是第一幅截图,我们可以看到程序4-2在执行以后系统给4-2这个进程分配了一个唯一的进程的ID:18246。 同时,进程启动了一个新的进程 vim:如图: 好,我们在另一个终端中使用 ps –a 命令查看一下,当前系统的进程: 部分截图如下: 我们发现 18246 进程竟然是 vim文本编辑器的进程,这是怎么回事?进程标识符为18246的进程明明是我们4-2的进程,怎么在系统中显示18246是vim呢?难道内核给他们分配了一个相同的进程标识符? 其实,内核并没有糊涂,原因是当4-2 进程启动的时候,内核给这个进程分配了一个进程标识符18246,而exec系统调用,在其执行的时候,内核会将新的进程取代原来的进程,也就是说,当执行exec系统调用的时候,内核会将原来的进程先关闭,回收被关闭的进程的进程号,然后按照exec的命令启动新的进程,并把刚才回收的进程标识符分配给这个新的进程,所以,我们看到的这两个进程的标识符是一样的就不奇怪了! 总结:使用exec家族函数创建新的进程后,会以新的程序取代原来的进程,然后系统会从新进程运行,新的进程的PID值会与原来进程的PID值相同。 fork系统调用 fork系统调用在linux中视最重要的系统调用之一,是我们实现进程控制的最常用的系统调用,并且,很多其他的系统调用也要使用到它! Fork系统调用说明 所需文件头: 函数功能: 函数原型: 函数传入之值 函数返回值 备注: #include 什么是叫继承父进程的一切呢?就是克隆父进程的所有,包括父进程的代码,父进程正在执行的状态,父进程的工作目录,父进程的所有资源等等,一切的一切,fork系统调用会全部的复制下来。 我们在来看fork函数的返回值,它好像有两个返回值,其实是一个,在调用fork系统调用后,原先的进程空间从一个变成两个,而fork函数在不同的进程空间会返回不同的值,在子进程的进程空间中,fork返回 0 ,而在父进程的进程中则返回刚刚新建的子进程的进程标识符,也就是子进程的pid。 我们看一个例子,这样能更好帮我们理解fork函数的作用! 例4-3要求设计一个程序,要求在显示当前目录下的文件信息,然后测试到一个任意社区的网络连接状况。 操作步骤: 步骤1:编辑源代码: [root@localhost char2]vim 4-3.c 程序源代码如下: #include pid_t result; result=fork(); int newret; if(result ==-1){ perror(\"创建子进程失败\"); exit; else if(result ==0) /*返回值为零,为子进程 */ { printf(\"返回值是:%d,这是子进程!\\n此进程ID号(pid)是:%d\\n此进程的父进程的ID(ppid)号是:%d\\n\ newret = system(\"ls -l\"); } else{ printf(\"返回值是:%d,这是父进程!\\n此进程ID号(pid)是:%d\\n此进程的父进程的ID(ppid)号是:%d\\n\ } } newret = system (\"ping www.163.com\"); 步骤2:编译并执行程序结果如下: 我们看到两个进程,父进程和子进程,父进程是4-3,而子进程是使用fork系统调用创建出来的子进程。 在父进程中打印了父进程自己的ID号,并且打印了子进程的ID号,然后做了一个ping命令,用于查看到163是否连通的测试! 在子进程中,子进程同时打印了自己的进程号和父进程的进程号,并做了一个ls当前目录的操作。 我们看到好像程序是在一个进程中完成的,其实,它是在两个进程中分别完成的。 下面,我们将看到一个有意思的东西,但是,这必须建立在我们理解了fork系统调用之后! 我们知道fork会创建子进程,一个和父进程一样但是完全独立的子进程。那么我们下面的代码: for( ; ; )fork(): 这是一个无限循环创建子进程的语句,我们想象一下,一旦我们把这样的语句写在一个程序中并且执行程序,那么那个进程就会在无限循环中创建子进程。 那么系统资源很快就会被不断创建的子进程占满,使系统无法继续正常工作,那么,以上就是一个简单的系统炸弹! 很简单,一句代码,你就可以做步入了黑客的殿堂! 当然,这里真正的黑客还差的很远,上面的程序破解起来也很简单,只要系统管理员对用户可以调用的进程数量进行限制就是破解! 有兴趣的同学可以自己下去实验!这里就不多讨论了! system系统调用: system系统调用说明: 所需头文件 函数功能: 函数原型 参数传入值 函数返回值 #include 其实,执行system系统调用,实际上内核调用了三个系统效用,首先调用 fork系统调用创建一个新子进程,然后使用execve系统调用到默认环境变量中寻找并执行传入参数命令,最后执行 waitpid等待返回执行结果。 其原理如下图所示: 4.3进程的终止 进程在执行结束时并不会自己结束,而需要使用进程终止的系统调用:“exit”与“_exit”. 我们在前面的程序中已经使用过“exit”系统调用,其功能就是退出进程,用法十分简单,但是我们还没见过“_exit”,在这里我们需要详细探讨一下它的使用以及这两个系统调用的区别。 我们直接通过一段程序来看其使用效果: 操作步骤: 步骤1:编辑源代码: [root@localhost char2]vim 4-4.c 程序源代码如下: #include pid_t result; result = fork(); if(result == -1){ perror(\"创建子进程失败\"); exit(0); else if(result == 0) printf(\"测试中止进程函数_exit函数!\\n\"); printf(\"这一句我们来看看缓存的结果!\"); _exit(0); else { sleep(10); printf(\"测试中止进程的函数exit!\\n\"); printf(\"这一句我们用来监察最后使用不通函数而得到的不通结果!\"); exit(0); 我们使用父子进程来分别调用这两个终止进程的系统调用,在子进程中我们调用_exit来终止子进程,在父进程中,我们使用exit来终止父进程。 步骤2:编译并执行程序,结果如下: 我们看到,使用_exit系统调用退出的进程少打了一句话:“这一句我们用来看看缓存的结果” 子进程在使用_exit之前没有执行printf函数吗?其实执行过了,printf函数是基于流操作的函数,也就是说他使用缓存,那么他的工作原理,就是先把要输出的语句全部放在缓存上,然后等待一个标志在把缓存上的数据一 口气输出到屏幕上,在子进程中,第一个printf语句有换行标志,那么当printf看到换行时,会把放在缓存上的信息输出到屏幕,而第二句,printf会一直等待一个标志,不过很可以,在没有等待到这个标志的时候,进程就被强制使用_exit系统调用退出了!由此printf抱憾终身! _exit系统调用的作用是直接使进程终止,清除其使用的内存空间,并清除其在内核中的各种数据结构。 而exit系统调用是一个安全的退出的函数。exit()函数则在退出前自动增加若干到工序,比如,系统在退出之前,它要查看查看文件的打开情况,并把缓冲区的内容写回文件,以便更好的保护数据的完整性。在确保所有文件安全后,exit才清除其在内核中的各种数据结构,并结束进程的运行。因此,在父进程中第二句printf虽然也没有标志告诉printf输出,但是exit在清空内存之前,做了一系列的安全检查,在确保进程的功能完全实现后,或者说文件安全后,在清空内存,终止进程。 4.4 特殊进程 4.4.1僵尸进程 僵尸进程(zombie)是指已终止运行,但尚未被清除的进程。 前面的学习中,我们已经了解了父进程和子进程的概念,并已经掌握了系统调用exit的用法,我们知道exit系统调用是安全终止进程的系统调用,它在真正终止进程之前要进行一些列的检查,其实,在一个进程调用了exit之后,该进程并完全消失掉(终止),而是留下一个称为僵尸进程(Zombie)的数据结构。僵尸进程是非常特殊的一种进程,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。从这点来看,僵尸进程虽然有一个很酷的名字,但它的影响力远远抵不上那些真正可怕的僵尸兄弟,它除了在进程列表表中留下一个供人凭吊的信息,其它对系统毫无作用。 让我们来看一眼Linux里的僵尸进程究竟长什么样子。 当一个进程已终止,但其父进程还没有调用系统调用wait(稍后介绍)对其进行收集之前的这段时间里,它会一直保持僵尸状态,利用这个特点,我们来写一个简单的小程序: 例 4-5 设计一个程序,要求使父进程休眠并没有回收子进程之前,子进程调用 exit系统调用终止进程。 操作步骤: 步骤1:编写源文件 [root@localhost char2]vim 4-5.c 程序源代码如下: #include pid_t pid; pid=fork(); if(pid<0) printf(\"error occurred!n\"); else if(pid==0) /* 如果是子进程 */ exit(0); else /* 如果是父进程 */ sleep(60); /* 休眠60秒,这段时间里,父进程暂停 */ wait(NULL); /* 收集僵尸进程 */ } 步骤2:编译并执行程序:如图所示: 我们看到,这是,父进程在休眠状态,还没有退出,但是子进程已经使用系统调用exit终止进程了 我使用使用查看进程的命令查看当前系统的进程列表: 在另一个终端中,输入: [root@localhost char2]ps -aux 得到的结果部分如图: 用黑线画出来的进程就是4-5子进程终止后留下的僵尸进程了!我们可以看到他的进程状态使用Z+来标记,说明他是一个僵尸进程。 注:{ linux上进程的5种状态: 1. 运行(正在运行或在运行队列中等待) 2. 中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号) 3. 不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生) 4. 僵死(进程已终止, 但进程描述符存在, 直到父进程调用wait()系统调用后释放) 5. 停止(进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行) ps工具标识进程的5种状态码: D 不可中断 uninterruptible sleep (usually IO) R 运行 runnable (on run queue) S 中断 sleeping T 停止 traced or stopped Z 僵死 a defunct (\"zombie\") process 其它状态还包括W(无驻留页), <(高优先级进程), N(低优先级进程), L(内存锁页)等. 现在,我们对exit系统调用有了新的认识:exit系统调用,它的作用是使进程终止,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁。 僵尸进程虽然对其他进程几乎没有什么影响,不占用CPU时间,消耗的内存也几乎可以忽略不计,但有它在那里呆着,还是让人觉得心里很不舒服。而且Linux系统中进程数目是有限制的,在一些特殊的情况下,如果存在太多的僵尸进程,也会影响到新进程的产生。 但是,为什么会有僵尸进程出现呢?当初设计UNIX(linux的僵尸进程是从unix中够继承的来的)的工程师仅仅是闲来无聊想烦烦其他的程序员吗?其实不然,我们可以想象,如果一个进程终止后,而此时程序员或系统管理员需要用到一些进程的相关信息,比如这个进程是怎么死亡的?是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?其次,这个进程占用的总系统CPU时间和总用户CPU时间分别是多少?发生页错误的数目和收到信号的数目。如果一个 进程终止所有与之相关的信息都立刻归于无形,那要使用上面所说的信息,就只好干瞪眼了。然而这些信息都被设计存储在僵尸进程中,如果需要,只要进程相关的资源回收就可以了! 但是,如果我们不需要回收这样的信息,并且系统中已经有了太多的僵尸,很多僵尸根本毫无用处,那么如何消灭这些僵尸进程呢? 那么,总结起来,我们有两个任务: 1. 对于必要的资源信息,我们需要先收集信息,再结束僵尸进程; 2. 对于根本不用的僵尸进程,我们直接结束僵尸进程。 不管是上面的哪一种情况,都要靠我们下面要讲到的waitpid调用和wait调用。这两者的作用都可以收集僵尸进程留下的信息,同时使这个进程彻底消失。下面就对这两个调用分别作详细介绍。 我们在父进程中可以调用wait或waitpid函数,这两个系统调用自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到 这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。 wait系统调用: wait会返回被收集的子进程的进程ID(如果执行成功),如果调用进程没有子进程,调用就会失败,此时wait返回-1。 wait系统调用说明: 所需文件头 函数原型: 函数传入值 返回值: #include 当我们不需要进程的相关信息的时候,我们可以使用NULL参数,也就是: pid = wait(NULL); 当我们需要收集相关资源信息的时候,就需要用到status参数。当参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,于是,人们就设计了一套专门的宏(macro)来完成这项工作,我们学习其中最常用的两个: ● WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值 ● WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义。 我们来看一个例子: 例4-6,僵尸进程回收实例: 操作步骤: 步骤1:编辑源代码文件: [root@localhost char2]vim 4-6.c 程序源代码如下: #include int main(){ int status; pid_t pc,pr; pc =fork(); if (pc<0){ perror(\"进程创建失败\"); exit(0); }else{ if(pc==0){ printf(\"这是子进程%d\\n\ } sleep(3); exit(0); }else if(pc>0){ pr=wait(&status); } if(WIFEXITED(status)){ printf(\"子进程%d正常终止,\ printf(\"中止时的状态是:%d\\n\TUS(status)); }else{ printf(\"子进程%d非正常中止\\n\} return 0; } } 步骤2:编译并执行程序,结果如下: 我们看到,程序将子进程退出时的状态打印出来了! 我们再来看看Waitpid系统调用 Waitpid系统调用说明: 所需文件头 函数原型 函数传入值: #include waitpid的返回值比wait稍微复杂一些,一共有3种情况: ● 当正常返回的时候,waitpid返回收集到的子进程的进程ID; ● 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; ● 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; 当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回 我们仔细比较一下wait与waitpid,就会发现wait函数实际上waitpid函数的一个特例。wait实际上是在内部利通特殊参数调用waitpid系统调用。 我们看一个例子: 操作步骤: 步骤1:编辑源代码文件: [root@localhost char2]vim 4-7.c 程序源代码如下: #include int main(){ } } pid_t pc, pr; pc = fork(); if(pc<0) printf(\"创建子进程失败\\n\"); else if (pc==0){ sleep(10); exit(0); else { /*使用一个循环来测试子进程是否退出*/ do{ pr = waitpid (pc,NULL,WNOHANG); if(pr==0){ printf(\"依附于此进程的子进程还没有中止.\\n\"); sleep(1); }while (pr==0); if(pr==pc){ printf(\"反回了子进程的ID:%d,说明子进程已经中止.僵尸资源已经回收,现在可以安全的中止父进程了\\n\ } } exit(0);} else printf(\"some error occured\\n\"); 步骤2:编译并执行程序,结果如下: 父进程经过10次失败的尝试之后,终于收集到了退出的子进程。 4.4.2守护进程 守护进程,也就是通常所说的daemon 进程,是Linux 中的后台服务进程,它运行在后台,并且一直在运行的一种特殊的进程。它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。 Linux 的大部分服务器就是用守护进程实现的,比如:Internet服务器,web服务器。 在Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,那么这个终端就称为这个进程的控制终端。当控制终端被关闭时,相应的进程就会随之自动关闭。 但是守护进程却能够突破这种界限,它从被执行开始运转,直到整个系统关闭时才会退出。因此:守护进程有如下特点: ● 守护进程常常是在系统引导装入时启动,且在后台运行,在系统关闭时终止。 那么,如果我们想让某个进程不因为用户或终端或其他的变化而受到影响,那么就需要把这个进程变成一个守护进程。 后台运行运行时守护进程的首要特性,其次,守护进程必须与其运行前的环境隔离开来,这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩码等。这些环境通常是守护进程从执行它的父进程中继承下来的。最后,守护进程有属于自己的启动方式,它可以在linux系统启动时从启动节本/etc/rc.d/rc.local中启动,也可以由作业规划进程crond启动,(就是定时管理启动)还可以由控制终端启动。 在linux环境下,有哪些内核守护进程,通过 ps –aux命令就可以查看 我们主要要认识一个守护进程 init init系统守护进程,他是进程1 ,负责启动运行层次特定的系统服务,这些服务通常是在他们自己拥有的守护进程的帮助下实现的。(可以说,这个进程是所有其他进程的父进程)。 (注意:大多数进程都是以超级用户(用户ID 号为0)特权运行,并且在后台运行其所对应的终端号显示为问号(?)。) 如图: 用黑线标出的就是我们的init守护进程,永远要记住的是:它是我们linux系统的所有进程的老祖宗进程。 在了解了守护进程的概念之后,我们的目的又是什么呢?我们要学习如何编写守护进程。 在前面的学习中,我们学习了僵尸进程,那么僵尸进程除了保存了一些相关的进程终止信息外,还有什么用处呢?它最重要的用法,就是帮助我们编写守护进程。记住一句话:守护进程是由僵尸进程改造而来的! 4.41.2编写守护进程 编写守护进程,相当于编写一个系统服务,编写守护进程的流程: 1 > 首先,创建子进程,终止父进程。 由于守护进程是脱离终端控制的,因此首先复制子进程,中止父进程,使得程序在shell里造成一个已经运行完毕的假象。之后所有的工作都在子进程中完成。这时候,程序进程是一个僵尸进程,在形式上做了与控制终端的脱离。 代码如下: pid =fork(); if (pid>0) exit(0); 2 > 在子进程中创建一个新的会话。 在调用fork函数时,子进程会全盘拷贝父进程的会话期,进程组,控制终端等,虽然父进程退出了,但是原先的会话期,进程组,控制终端等并没有改变,因此,并不是真正意义的独立开来。 进程组:是一个或多个进程的集合,进程组由进程组ID来唯一标识。除了进程号,进程组ID也是一个进程的必备属性。 会话期:会话组是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行所有进程都属于这个会话期。 使用setsid函数能够是进程完全独立开来,从而脱离所有其他进程的控制。setsid用于创建一个新的会话,并担任该会话组的组长。调用setsid有三个作用: 1,让进程摆脱原会话控制; 2,让进程摆脱进程组的控制; 3,让进程摆脱原控制终端的控制。 3 > 改变工作目录 调用fork函数子进程也会拷贝父进程的工作目录,通常的做法是把“/”目录作为守护进程的当前工作目录。特许需要则,也可以使用其他的工作目录。 改变工作目录的函数是:chdir. 4 > 重设文件创建掩码 fork函数会复制父进程的文件掩码,这就给该子进程使用文件带来了诸 多麻烦,因此,把掩码设置为0,可以大大曾加该守护进程的灵活性。设置掩码的函数是umask。 5> 关闭文件描述符 如果父进程已经打开一些文件,那么子进程会复制这些已经打开的文件,但是,这些被打开的文件可能永远不会被守护进程读或写,但是打开一样消耗系统资源,可能导致所在文件系统无法卸载。用如下方法关闭文件描述符。 for(i=0;i 我们来看一个实例: 步骤1: 编写守护进程创建函数: [root@localhost char2]vim init.c 程序源代码如下:init.c守护进程创建函数 #include pid_t child1,child2; int i ; child1=fork(); if(child1>0) exit(0); else if(child1<0) { perror(\"创建子进程失败\"); exit(1); } setsid(); chdir(\"/tmp\"); umask(0); for(i=0;i #include 程序如果成功执行,我们将在/tmp下,看到我们的日文件。如图: 我们查看其内容,结果如图所示: 我们看到,我们编写的守护进程正在不断的想日志文件中输写我们自定义的 日志内容。 我们再来看一下进程列表,如图: 注意黑线标出的部分,我们看到它的终端显示号为“?”,说明它已经脱离了终端的控制,成为我们系统的守护进程了! 要想关闭它,我们只能使用强制关闭进程的命令:kill 进程号!例如,上例中,可使用: [root@localhost char2]kill 17694 或者关闭计算机。 第五章 进程间的通信 IPC 一个大型的应用系统,往往需要众多进程协作,进程间的通信往往是必须的,这里的进程一般是指运行在用户状态的进程,而由于处于用户状态的不同进程之间是彼此隔离的,就像处于不同城市的人们,他们必须通过某种方式来提供信息,例如手机。 纯理论: Linux下的进程通信手段基本上是从UNIX 平台的进程通信手段继承而来的。 UNIX发展做出贡献的两大主力: 贝尔实验室和加州大学伯克利分校的伯克利软件发布中心 前者对UNUX早期的进程通信手段进行了系统的改进和扩充,形成了“system V IPC”其通信进程主要局限在单个计算机内,后者测跳过了该限制,形成了基于套接字(socket)的进程通信机制。而Linux则把两者的优势都继承了下来。 Linux中使用较多的进程间的通信方式主要有以下几种: 1. 管道(Pipe):包括有名管道和无名管道:无名管道可以用于具有亲缘关系进程间的通信,有名管道,除了上述通信外,还允许无亲缘关系的进程间的通信。 2. 信号: 信号是软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知接收进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求的效果可以说是一样的。 3. 消息队列:消息队列是消息的链接表,包括POSIX消息队列system V消息队列。它克服了前两种通信方式中信号量有限的缺点,具有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读取消息。 4. 共享内存:可以说这是最有用的进程间的通信方式,它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。 5. 信号量:主要作为进程间以及同一进程不同线程之间的同步手段。 6. 套接字:socket主要用于不同机器之间进程间的通信,应用比较广泛。 5.1 管道通信 5.1.1什么是管道? 管道就是把一个程序(进程)的输出连接到另一个程序(进程)的输入!比如: ps –ef |grep ntp,在这条系统命令中,我们使用了“|”管道符,它的意思就是将“ps –ef”这个程序进程的输出结果,输入给下一个程序进程“grep ntp”。那么,两个进程通过“|”管道符的连接,实现了ps进程到grep进程的通信,最终的执行结果是两者结合的结果。 管道是Linux支持的最初Unix IPC形式之一,具有以下特点:(无名管道) 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道; 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程); 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。 管道的创建与关闭管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fds[0]和fds[1] ,其中fds[0]固定用于读取管道,fds[1]固定用于写管道。与串口通信一样,一个读,一个写,这样就构成了一个半双工的通道。管道关闭时只需将这两个文件描述符关闭即可,可使用普通的close函数逐个关闭两个文件描述符。 管道的创建函数: pipe 所需文件头: 函数原型: 函数传入值: 函数返回值: #include 例 7-1 ,管道的创建: #include int main(){ int pipe_fd[2]; if (pipe(pipe_fd)<0){ } else } printf(\"管道创建成功,成功打开%d读和%d写\\n\close(pipe_fd[0]); printf(\"管道读关闭\\n\"); close(pipe_fd[1]); printf(\"管道写关闭\\n\"); printf(\"创见管道失败\\n\"); return -1; 管道的读写: 虽然管道创建是包含读读管道和写管道,并且,当这两个管道被建立起来的时候,就已经默认建立了连接,但是,我们说管道是用在不同进程间通信的,用pipe创建的管道两端处于一个进程中,因此,在一个进程中的管道好像显得毫无意义,其实 在实际应用中,先创建一个管道,在通过fork()函数创建一个子进程,子进程会复制父进程所创建的管道,这时,父子进程管道的文件描述符对应关系如图: 这时,关系看似十分复杂,,但实际上却已经给不同进程之间的读写创造了很好的条件。这时,父进程分别拥有自己的读写管道,为了实现父子进程之间的 读写,只需把无关的读端或写端的文件描述符关闭即可,如图: 如图所示:当我们关闭父进程的写管道与子进程的读管道的时候,这样在父进程和子进程之间建立一条“子进程写入父进程读”的通道。同样,关闭父进程fd[0]和子进程的fd[1]; 这样就建立了一条“父进程写入子进程读”的通道。此外:父进程还可以创建多个子进程,各个子进程之间都继承了相应的fd[0]和fd[1],这时,只需要关闭相应的端口就可以建立起各子进程之间的通道。 列子:7-2,实现父子进程的通信。 操作步骤: 步骤1:编辑源文件: [root@localhost ~]vim 7-2.c 程序源代码如下: #include memset(buf_r, 0,sizeof(buf_r)); if(pipe(pipe_fd)<0){ printf(\"pipe create error\\n\"); } return -1; if((pid=fork())==0){ printf(\"\\n\"); close(pipe_fd[1]); } } } } sleep(2); if((r_num=read(pipe_fd[0],buf_r,100))>0){ printf(\"从管道中读取数据成功!\\n\"); printf(\"%d字节数从管道中读出,内容如下:%s\\n\ close(pipe_fd[0]); exit(0); else if(pid>0){ close(pipe_fd[0]); if(write(pipe_fd[1],buf_w,sizeof(buf_w))!=-1) printf(\"向管道中写数据成功!\\n\"); close(pipe_fd[1]); waitpid(pid,NULL,0); exit(0); 步骤2:编译并执行程序:结果如下: 5.1.2管道的读写规则: 我们已经知道,管道两端可分别用描述字fd[0]以及fd[1]来描述,一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述 字fd[1]来表示,称其为管道写端。那么,管道通信的第一条规则就是如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。 从管道中读取数据: 如果管道的写端不存在:则认为已经读到了数据的末尾,读函数返回的读出字节数为0; 当管道的写端存在时: 1.> 如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数; 2. >如果请求的字节数目不大于PIPE_BUF,且管道中数据量小于请求的数据量时,则返回管道中现有数据字节数; 3.> 如果请求的字节数目不大于PIPE_BUF,且管道中数据量不小于请求的数据量,此时返回请求的字节数。 4.>当写管道存在时,并且已经向管道写了数据,那么管道写端关闭后,写入的数据将一直存在,直到读出为止. 注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。red hat中为4096)。 读管道读规则实验:7-2.read [root@localhost ~]vim 7-2read.c 程序源代码如下: #include pid_t pid; char r_buf[100]; char w_buf[4]; char *p_wbuf; int r_num; int cmd; memset(r_buf,0,sizeof(r_buf)); memset(w_buf,0,sizeof(w_buf)); p_wbuf=w_buf; if(pipe(pipe_fd)<0){ printf(\"pipe 创建失败!\\n\"); return -1; } if((pid=fork())==0) { printf(\"\\n\"); } else if(pid>0){ close(pipe_fd[0]); strcpy(w_buf,\"111\"); if(write(pipe_fd[1],w_buf,4)!=-1){ printf(\"父进程写入成功\\n\"); } close(pipe_fd[1]); printf(\"关闭了父进程的写管道\\n\"); sleep(10); wait(NULL); close(pipe_fd[1]); sleep(3);//确保父进程关闭写端 r_num=read(pipe_fd[0],r_buf,100); printf(\"读出的字符数是:%d,读出的数据是:%d\\n\close(pipe_fd[0]); exit(0); } return 0; } 编译并执行程序:结果如下: 我在子进程中请求读100个字符,但是只返回了4个字符大小,因为管道中只有4个字符大小。 向管道中写入数据: 向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。 注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。 对管道的写规则的验证1:写端对读端存在的依赖性 [root@localhost ~]vim 7-2write.c 程序源代码如下: #include #include int pipe_fd[2]; pid_t pid; char r_buf[4]; char* w_buf; int writenum; int cmd; memset(r_buf,0,sizeof(r_buf)); if(pipe(pipe_fd)<0) { printf(\"管道创建失败\\n\"); return -1; } pid=fork(); if(pid==0) { printf(\"这是子进程!\\n\"); close(pipe_fd[0]); close(pipe_fd[1]); sleep(10); exit(0); } } else if(pid>0) { } printf(\"这是父进程!\\n\"); sleep(2); close(pipe_fd[0]); w_buf=\"111\"; if((writenum=write(pipe_fd[1],w_buf,4))==-1){ perror(\"写管道失败\\n\"); } else{ printf(\"写管道成功,写了%d个字符\ } close(pipe_fd[1]); wait(NULL); exit(0) ; 编译程序并运行,结果如下: 我们在程序中让子进程休眠10秒,但是我们运行程序发现,进程在运行了大约2秒后被终止了,因为在父进程休眠了2秒后,父进程试图在向写管道中写入数据,但是子进程的读管道已经被关闭,所以父进程写管道出错,并发射SIFPIPE信号,使父进程终止。 对管道的写规则的验证2:linux不保证写管道的原子性验证 [root@localhost ~]vim 7-2atom.c 程序源代码如下: #include int pipe_fd[2]; pid_t pid; char r_buf[4096]; char w_buf[4096*2]; int writenum; int rnum; int i=0; memset(r_buf,0,sizeof(r_buf)); if(pipe(pipe_fd)<0) { printf(\"管道创建失败!\\n\"); return -1; } if((pid=fork())==0) { close(pipe_fd[1]); while(i<20) { } sleep(1); rnum=read(pipe_fd[0],r_buf,1000); printf(\"子进程:读出了%d个字符.\\n\i++; close(pipe_fd[0]); exit(0); } if(pid>0) { printf(\"这是父进程\\n\"); } close(pipe_fd[0]); memset(w_buf,1,sizeof(w_buf)); writenum=write(pipe_fd[1],w_buf,1024); if(writenum==-1){ else printf(\"写管道出错\\n\");} {printf(\"向管道写入了%d个字符\\n\ writenum=write(pipe_fd[1],w_buf,4096); close(pipe_fd[1]); 编译并执行程序,结果如下: 从结果我们可以看出,linux不保证写管道的原子性验证,如果把父进程中的两次写入字节数都改为5000,则很容易得出下面结论: 写入管道的数据量大于4096字节时,缓冲区的空闲空间将被写入数据(补齐),直到写完所有数据为止,如果没有进程读数据,则一直阻塞。 5.1.3高级管道操作: 在Linux中除了常用的pipe函数建立管道外,还可以使用高级管道操作popen函数来建立管道,这样,管道的操作也就成了基于文件流的模式,这种基于文件流的管道主要是用来创建一个连接到另一个进程的管道。这里的另一个进程也就是一个可以进行一定操作的的可执行文件,如“ps -ef”或自己编辑的程序的等。 popen所完成的工作有以下几点: 1. 创建一个管道; 2. fork一个子进程; 3. 在父进程中关闭不需要的文件描述符; 4. 执行exec家族函数调用 5. 执行函数中所制定的命令; Popen系统调用格式: 所需文件头 函数原型 函数传入值 #include 列:7-3,在前面我们经常看到“ls -l | grep 7-3 ”的用法,其中,我们知道“|”是管道符那么,我们就以这个命令为例,看看这个管道符到底做了些什么? 分析:我们要把ls -l的输出连接到grep 7-3的输入,那么对于ls -l 而言,管道的类型应该是r和w中的哪一个?很明显,我们要做的是需要命令ls -l 产生输出,因此,对于进程ls -l而言,需要的参数类型是:r . 对于grep 7-3来说呢?我们需要把前一个进程的结果输入到grep 7-3这个进程当中,所以对于进程grep 7-3建立的管道而言,参数的类型应该是:w。 操作步骤: 步骤1:编译原文件 [root@localhost ~]vim 7-3.c 程序源代码如下: #include FILE *fp; int num; char buf[500]; memset(buf,0,sizeof(buf));/*初始化清空操作(把buf所指的内存区域的前sizeof(buf)得到的字节设置成0)*/ printf(\"建立管道^^^^\\n\"); fp=popen(\"ls -l\printf(\"%s\\n\if(fp!=NULL){ num=fread(buf,sizeof(char),500,fp); if(num>0){ printf(\"第一个明令是:ls -l,运行结果如下:\\n\"); } } else{ printf(\"%s\\n\ } pclose(fp); printf(\"创建管道失败\"); return -1; } fp=popen(\"grep 7-3\ printf(\"第二个命令是:grep 7-3,运行结果如下: \\n\"); fwrite(buf,sizeof(char),500,fp); pclose(fp); return 0; 编译并执行程序结果如下: 命名管道:FIFO 前面介绍的无名管道,是运行于具有亲缘关系的进程之间,这就大大限制了管道的使用,若在两个不相关的进程之间使用管道,则需要用到命名管道。 命名管道在文件系统是可见的,并且可以通过路径名来指出其位置。 命名管道的创建可以使用函数mkfifo(),该函数类似open()操作,可以指定管道的路径和打开模式。两个进程可以把它当作普通文件一样来进行读写操作。 注意:FIFO是严格的遵循先进先出规则的,对管道及FIFO的读总是从开始返回数据,对他们的写则把数据添加到末尾,他们不支持lseek()等文件操作。 在创建管道成功以后,就可以使用open,read,write这些函数了,与普通文件一样,打开管道文件时,对为读而打开的文件可以在open中设置O_RDONLY, 对 于写而而打开的文件在open中设置O_WRONLY. 需要注意的是,FIFO与普通文件的不同点:阻塞问题。 由于普通文件的读写时不会出现阻塞问题,而在管道中读写却有可能,这里的非阻塞标志可以在open函数中设定为O_NONBLOCK。后面我们会对阻塞打开和非阻塞打开的读写进行一定的讨论。 5.1.4 有名管道的读写规则 从FIFO中读取数据: 约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。 如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。 对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论写入数据量的大小,也不论读操作请求多少数据量。 读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。 如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。 注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。 向FIFO中写入数据: 约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。 对于设置了阻塞标志的写操作: 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。 对于没有设置阻塞标志的写操作: 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写; mkfifo函数格式: 所需文件头 函数原型: 函数传入值 #include 在对FIFO出错信息做一个归纳,以方便检查: EACCESS EEXIST ENAMETOOLONG ENOENT ENOSPC ENOTDIR EROFS 参数filename所指定的目标路径无可执行的权限 参数filename所指定的文件一存在 参数filename的路径名称太长 参数filename包含的目录不存在 文件系统剩余空间不足 参数filename路径中的目录存在但却非真正的目录 参数filename指定的文件存在于只读文件系统内 例7-4 一个简单聊天程序。 管道式单双工的,所以两个程序都需要建立相同的两个管道文件 操作步骤: 步骤1:编译源代码: [root@localhost ~]vim 7-4zhang.c 程序源代码如下: #include wfd=open(\"fifo1\以写方式打开管道*/ rfd=open(\"fifo2\以读方式打开管道*/ if(rfd <= 0 || wfd <= 0) return 0; printf(\"这是张三端!\\n\"); while(1){ FD_ZERO(&read_fd);/*清空一个文件描述符集*/ FD_SET(rfd,&read_fd);/*将文件描述符加入文件描述符集read_fd*/ FD_SET(fileno(stdin),&read_fd); net_timer.tv_sec=5; net_timer.tv_usec=0; memset(str,0,sizeof(str)); if((i = select(rfd+1,&read_fd,NULL,NULL,&net_timer)) <= 0) continue; if(FD_ISSET(rfd,&read_fd)) /*判断rfd是否可读写*/ { read(rfd,str,sizeof(str));/*读取管道,将管道内容存入str*/ printf(\"李四: %s\\n\ int i,rfd,wfd,len=0,fd_in; char str[32]; int flag,stdinflag; fd_set write_fd,read_fd; struct timeval net_timer; mkfifo(\"fifo1\ mkfifo(\"fifo2\ } if(FD_ISSET(fileno(stdin),&read_fd)) { } } } printf(\"-------------------------------------\\n\"); fgets(str,sizeof(str),stdin); len=write(wfd,str,strlen(str));/*写入管道*/ close(rfd); close(wfd); 步骤2:编写李四端的源文件: [root@localhost ~]vim 7-4li.c 程序源代码如下: #include