您的当前位置:首页正文

Linux 应用程序开发

2021-06-01 来源:独旅网
Linux 应用程序开发

引言

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 main() { float fahr , celsius ; /* 定义华氏与摄氏符点型变量 */ }

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 /*文件预处理,包含自定义的库文件*/ int main(){ char ch; while ((ch=getchar())!=EOF) putchar(ch); }

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语句中出现“”,那么gcc将去“/usr/include”目录下寻找名为“my.h”的头文件。

此时,我们自定义的头文件在目录“/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 #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 void main(){ long long tmp=1; printf(“这是一段用于测试警告和出错参数的程序”); }

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 int main(){ double testnum; double result; double temp; } }

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(xreturn y

}

步骤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 #include #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 //提供类型p_id的定义 #include //提供文件属性的定义 #include //提供文件控制的相关定义 函数功能 函数原型 打开(并)创建文件(视参数而定) int open(const char *pathname,int flag) int open(const char *pathname,int flag,mode_t mode) int open(const char *pathname,int flag,int perms) 函数传入值: 函数返回值 备注 建立文件的访问路径,用来设置新增文件的权限 正确返回文件描述符,有错误发生返回-1。 表2-3-2 open函数用法

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 关闭文件 Int close(int fd) 文件描述符 正确(文件顺利关闭)返回0,有错误发生返回-1。 open和close的使用实例:

例:2-1,要求打开文件/home/2-1file,如果文件不存在就创建此文件,如果文件存在,清空数据后,并且使用可写方式打开,同时设定文件的权限0600. 操作步骤: 1. 编辑源文件2-1

[root@localhost char2]vim 2-1.c 2 . 源程序代码如下:

#include

#include /*包含system函数库*/ #include #include #include

#include /*包含open函数库*/ int main(){ int fd; } } }

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 /*包含system函数库*/ #include #include #include

#include /*包含open函数库*/ int main(){

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 读取文件 Ssize_t read(int fd,void *buf , size_t count); fd 文件描述符 buf 读出数据的缓冲区 count 指定读出的字节数 函数返回值 成功:返回读到的字节数。 0 :已达到文件尾 (在读普通文件时,若读到要求的字节数之前已经达到文件的尾部,则返回的字节数回小于希望读出的字节数) 有错误发生返回-1。 备注

write函数说明 所需文件头: 函数功能 函数原型 函数传入值: #include 写入文件 Ssize_t write(int fd,void *buf , size_t count); fd 文件描述符 buf 写入数据的缓冲区 count 指定写入出的字节数 函数返回值 备注

lseek函数说明 所需文件头: 函数功能 函数原型 函数传入值: #include #include 定位指针位置 off_t lseek(int fd, off_t offset, int whence); fd 文件描述符 offset 偏移量,每一读写操作所需要移动的距离,单位是字节的数量,可正负,(向前,向后) whence 当前位置的基点() 函数返回值 备注 成功:文件的当前位移。 有错误发生返回-1。 Whence参数说明 SEEK_SET 当前位置为文件的开头,新位置为偏移量的大小 SEEK_CUR 当前位置为文件指针的位置,新位置为当前位置加上偏移量的大小 SEEK_END 当前位置为文件的末尾,新位置为当前位置加上偏移量的大小

成功:返回写入的字节数。 有错误发生返回-1。 例2-3打开/etc/passwd用read 函数读取文件内容,建立新文件,并使用write把读到的内容写入新文件 操作步骤:

1. 编辑源程序: [root@localhost char2]vim 2-3.c 程序源代码如下: #include #include /*包含system函数库*/ #include

#include #include

#include /*包含open函数库*/ int main(){ } } } } }

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 #include #include 对文件上锁(及文件进程相关控制) int fcntl(int fd, int cmd, struct flock *lock) fd 文件描述符 cmd 命令(参看以下具体) lock 结构为flock,设置记录所的具体状态 成功返回0,有错误发生返回-1。 给整个文件加锁的时候,通常方法是将l_start设为0,l_whence为SEEK_SET,l_len设为0 CMD F_DUPFD 说明 复制文件描述符 函数功能 函数原型 函数传入值: 函数返回值 备注 F_GETFD F_SETFD F_GETFL F_SETFL F_GETLK F_SETLK F_SETLKW 获得文件描述符close-on-exec标志。 设定文件描述符close-on-exec标志。 得到open()的参数flags 设置open()的参数flags 根据LOCK的描述,决定是否上文件锁 设置LOCK描述的文件锁 F_SETLK的阻塞版本,W表示等待,如果存在其他的锁,则调用进程睡眠,如捕捉到信号则睡眠中断。 例2-4 文件上锁示例! 操作步骤:

步骤1:编辑源文件: [root@localhost char2]vim 2-4.c 程序源代码如下:

#include #include #include #include #include #include void lock_set(int fd,int type){

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 #include 创建目录文件 int mkdir(const char* pathname,modet_ mode); pathname:目录名。mode目录权限 成功返回0,失败返回-1. 备注: rmdir 函数说明: 所需头文件 函数功能: 函数原型: 函数传入值: 函数返回 #include 删除目录文件 int rmdir(const char* pathname); pathname:目录名 成功返回0,失败返回-1. 备注:

opendir函数说明: 所需头文件 函数功能: 函数原型: 函数传入值: #include #include 打开目录文件 DIR *opendir(const char* name); 打开参数name指定的目录,并返回DIR*形态的目录流,和open()类似,接下来对目录的读取和搜索都要使用此返回值。 成功返回DIR*形态的目录流指针,打开失败则返回空。 函数返回 备注:

readdir函数说明: 所需头文件 #include 函数功能: 函数原型: 函数传入值: 返回返回值 #include #include 读取该目录 struct dirent* readdir(DIR * dir); 被打开的文件目录流指针。 返回一个dirent的结构体。此结构体定义如下: struct dirent { long d_ino; //索引节点号 off_t d_off; //在目录文件中的偏移 unsigned short d_reclen; //文件目录的长度 char d_name [NAME_MAX+1]; //文件目录名 } 备注:

closedir函数说明: 所需头文件 函数功能: 函数原型: 函数传入值: 函数返回 #include #include 关闭目录文件 int closedir(DIR *dir); 被打开的文件目录流指针。 成功返回0,打开失败则返回-1。 备注:

例 2-6 设计一个程序,要求创建一个目录,并读取系统目录文件“/etc/rc.d”中所有的目录结构! 操作步骤:

步骤1:编辑源文件 [root@localhost char2]vim 2-6.c 程序源代码如下: #include #include #include #include #include int main(){ DIR * dir1; struct dirent *ptr; int i ; mkdir(\"/root/test\printf(\"创建目录成功!\\n\"); }

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 建立软链接 函数原型: 函数传入值: 函数返回 int symlink(const char* oldpath , const char* newpath); 参数newpath:衔接的名称 参数 oldpath:已存在文件路径和文件名 成功返回0,失败返回-1. 参数oldpath指定的文件一定要存在,否则无法建立有效的连接!newpath存在则会建立链接失败。 备注: 例 2-7. 建立软链接“2-7link”,并为当前目录下的一个任意文件建立链接。 操作步骤:

步骤1 :编译源文件: [root@localhost char2]vim 2-7.c 程序源代码如下: #include int main(){ int r; r=symlink(\"2-1.c\ if(r<0){ perror(\"链接失败\"); exit(1); } return 0; } 步骤2:编译并执行程序结果如下:

2.硬链接文件:

硬链接文件大体上和软连接执行的效果差不多,但是硬链接不允许给目录建立链接并且不能跨系统。 link系统调用,

所需头文件 函数功能: 函数原型: 函数传入值: #include 建立硬链接 int link(const char* oldpath , const char* newpath); 参数newpath:衔接的名称 参数 oldpath:已存在文件路径和文件名

函数返回 成功返回0,失败返回-1. 备注:

例 2-8,设计一个程序,要求为系统中某一文件建立硬链接,并查看其属性! 操作步骤:

步骤一:编译源文件: [root@localhost char2]vim 2-7.c 程序源代码如下: #include int main(){ int r; r=link(\"2-1.c\ if(r<0){ perror(\"链接失败\"); exit(1); }else{

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 #include #include #include \"../header/ourhdr.h\" int main(void) { umask(0); if (creat(\"foo\S_IROTH | S_IWOTH) < 0) perror (\"creat error for foo\"); umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if (creat(\"bar\S_IROTH | S_IWOTH) < 0) perror (\"creat error for bar\"); exit(0); } 2.5.3 文件权限相关函数 4. chmod和f c h m o d函数

这两个函数使我们可以更改现存文件的存取许可权。

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 #include #include \"../header/ourhdr.h\" int main(void) { struct stat statbuf; if (stat(\"foo\ perror(\"stat error for foo\"); if (chmod(\"foo\ perror (\"chmod error for foo\"); if (chmod(\"bar\ perror (\"chmod error for bar\"); exit(0); } 5. chown, fchown和l c h o w n函数

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 #include #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 #include #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 #include #include #include #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 取得当前进程的进程号 int getpid(); 无 成功返回当前进程的进程号,失败将错误包含在perror中 备注: getppid系统调用说明: 所需头文件 函数功能: #include 取得当前进程的父进程号 函数原型: 函数传入值: 函数返回 int getppid(); 无 成功返回当前进程的父进程号,失败将错误包含在perror中 备注: 例4-1,设计一个程序,显示其进程的标识符PID以及其父进程的标识符ppid! 操作步骤: 步骤1:编译源文件: [root@localhost char2]vim 4-1.c 文件源代码如下: #include #include int main(){ } printf(\"系统分配的进程号是:%d\\n\printf(\"系统分贝的父进程号是:%d\\n\return 0; 步骤2:编译并执行程序结构如下: 4.2.1 进程的创建

在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 创建并执行命令的进程 int execl(const char *path , const char *arg , „) int execv (const char *path , char *const argv[ ]) int execle(const char *path, const char *arg , „ , char*const envp[ ]) int execve(const char *path, char*const argv[ ], char*const envp[ ] ) int execlp(const char *file ,const char *arg , „) 函数返回值 int execvp(const char *file , char * const argv[ ]) -1:出错 这六个参数在语法上都有细致的区别,下面就可执行文件查找方式,参数的传递方式,及环境变量这几个方面进行比较。 1.>查找方式:

2.>参数的传递方式:

这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身),要注意的是. 这些参数必须以NULL表示结束,如果是列举方式,那么要把它强制转化成一个字符指针。

3.>环境变量:

exec函数可以默认系统的环境变量,也可以转入指定的环境变量。由e,p两个参数控制。

其中最常用的是:execve 以参数传递为构造指针数组方式,并且可以传递新进

参数传递方式有两种,l — 参数传递为逐个列举方式,v —参数传递为构造指针数组方式,

在表中,前4个参数的查找方式都是完整的文件目录路径,而最后两个是可以只给出文件名,系统就会到默认的环境变量“$PATH”所指的路径中进行查找。

程的环境变量。我们看下面的例子: 例4-2,创建一个进程,并返回新进程的pid! 操作步骤:

步骤1:编辑源文件:

[root@localhost char2]vim 4-1.c 程序源代码如下:

#include #include int main(){ }

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 #incude 建立一个新进程 Pid_t fork(void); 无 执行成功则建立一个新的进程,在子进程中则返回0,在父进程中回返回新建子进程的进程号(PID),失败则返回-1 新进程的信息是复制而来,而非指相同的内存空间,因子进程对这些变量的修改和父进程并不同步。 我们知道,进程必须有依托生存的环境,也就是其父进程,每一个进程必须在其父进程的照顾下才能生存,一旦失去其依托的进程,那么进程就失去了原有的功能,成为一个即浪费资源,占用空间而又没有任何贡献的行尸走肉——我们叫它——僵死进程。 fork函数的作用是在其进程中创建一个新的进程,这个新的进程不会取代原来的进程,而是以当前进程的一个子进程而存在的。这个子进程会有一个新的进程标识符pid。并且这个子进程会继承父进程的一切!

什么是叫继承父进程的一切呢?就是克隆父进程的所有,包括父进程的代码,父进程正在执行的状态,父进程的工作目录,父进程的所有资源等等,一切的一切,fork系统调用会全部的复制下来。

我们在来看fork函数的返回值,它好像有两个返回值,其实是一个,在调用fork系统调用后,原先的进程空间从一个变成两个,而fork函数在不同的进程空间会返回不同的值,在子进程的进程空间中,fork返回 0 ,而在父进程的进程中则返回刚刚新建的子进程的进程标识符,也就是子进程的pid。 我们看一个例子,这样能更好帮我们理解fork函数的作用!

例4-3要求设计一个程序,要求在显示当前目录下的文件信息,然后测试到一个任意社区的网络连接状况。 操作步骤:

步骤1:编辑源代码:

[root@localhost char2]vim 4-3.c 程序源代码如下:

#include #include #include #include int main() { }

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 在进程中开启一个进程 int system(const char *string) 系统命令(变量) 执行成功则返回执行shell命令后的返回值,调用/bin/sh 失败则返回127,其他失败返回-1,参数为空,则返回非零值。 我们在前面也使用过很多次system系统调用了,他的用法非常简单,只需要传入我们要执行的命令的字符串就可以顺利执行,我们在这里不是要学习它的用法,而是要理解system系统调用的工作原理!

其实,执行system系统调用,实际上内核调用了三个系统效用,首先调用 fork系统调用创建一个新子进程,然后使用execve系统调用到默认环境变量中寻找并执行传入参数命令,最后执行 waitpid等待返回执行结果。

其原理如下图所示:

4.3进程的终止

进程在执行结束时并不会自己结束,而需要使用进程终止的系统调用:“exit”与“_exit”.

我们在前面的程序中已经使用过“exit”系统调用,其功能就是退出进程,用法十分简单,但是我们还没见过“_exit”,在这里我们需要详细探讨一下它的使用以及这两个系统调用的区别。

我们直接通过一段程序来看其使用效果: 操作步骤:

步骤1:编辑源代码:

[root@localhost char2]vim 4-4.c 程序源代码如下: #include #include #include int main(){ } { } } }

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 #include #include #include #include main() {

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 #include Pid_t wait(int *status) 这里的status是一个整型指针,用于保存该进程退出的状态 成功:子进程的进程号 失败:-1

当我们不需要进程的相关信息的时候,我们可以使用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 #include #include #include #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 #include Pid_t waitpid(pid_t pid , int*status,int options) pid Pid>0: 只等待进程ID等于子pid的子进程,不管已经有其他子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。 Pid=-1:等带任何一一个子进程结束。此时和wait作用一样。 Pid=0:等待其组ID等于调用进程的组ID的任一子进程 Pid < -1: 等待其组ID等于pid的绝对值得任一进程 stuts option 等同于wait的status. WNOHANG:若由pid指定的子进程没有终止,则立刻返回0,不予等待。 WUTRACED若实现某支持作业控制,则由pid指定的任何一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态。 函数返回值: 0,同wait,阻塞父进程,等待子进程退出 正常:子进程的进程号 使用选项WNOHANG且没有子进程退出:0 出错:-1

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 #include #include #include #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例 6-1 创建一个守护进程,此进程在像/tmp下属写日志文件。

我们来看一个实例:

步骤1: 编写守护进程创建函数:

[root@localhost char2]vim init.c 程序源代码如下:init.c守护进程创建函数

#include #include #include #include #include void init_daemon(void){ }

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步骤2 ;创建程序主程序: [root@localhost char2]vim 6-1.c 程序源代码如下:

#include #include void init_daemon(void); int main(){ FILE *fp; time_t t; init_daemon(); while(1){ } sleep(10); fp=fopen(\"6-1.log\ if(fp>=0){ t=time(0); fprintf(fp,\"守护进程仍在运行,时间是:%s\ fclose(fp); } } 步骤3:编译并执行程序:

程序如果成功执行,我们将在/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 int pipe(int fd[2]) fd[2] :管道的两个文件描述符,之后就可以直接操作这两个文件描述符。 成功:0 失败:-1

例 7-1 ,管道的创建:

#include #include #include #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 #include #include #include #include int main(){ int pipe_fd[2]; pid_t pid; char buf_r[255]; char buf_w[]=\"hello,pipe\"; char *p_wbuf; int r_num;

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 #include #include #include int main(){ int pipe_fd[2];

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 #include #include

#include int main(){

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 #include #include main (int argc,char**argv) { }

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 FILE *popen(const char * command, const char *type) Command :指向的是一个以NULL结束的字符串,这个字符串包含一个shell命令,并被送到“/bin/sh”以参数-c执行。 type “r”:文件指针连接到command的标准输出,即该命令的结果产生输出; “w”:文件指针链接到command的标准输入,即该命令的结果产生输入; 返回值: 成功:文件流指针 失败: -1; 注意:如果使用popen创建管道必须使用标准I/O函数进行操作。 pclose 所需文件头 函数原型 返回值: #include FILE pclose(FILE *stream) 函数传入值: stream:要关闭的文件流 成功:文件流指针 失败: -1;

列: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 int main(){

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 #include int mkfifo(const char *filename, mode_t mode) filename :要创建的管道 mode: 参数mode S_IRUSR S_IWUSR S_IXUSR S_IRGRP S_IWGRP S_IXGRP S_IROTH S_IWOTH S_IXOTH 说明 拥有者具有读权限 拥有者具有写权限 拥有者具有执行权限 组用户具有读权限 组用户具有写权限 组用户具有执行权限 其他用户具有读权限 其他用户具有写权限 其他用户具有执行权限 函数返回值: 成功:0 出错:-1

在对FIFO出错信息做一个归纳,以方便检查: EACCESS EEXIST ENAMETOOLONG ENOENT ENOSPC ENOTDIR EROFS 参数filename所指定的目标路径无可执行的权限 参数filename所指定的文件一存在 参数filename的路径名称太长 参数filename包含的目录不存在 文件系统剩余空间不足 参数filename路径中的目录存在但却非真正的目录 参数filename指定的文件存在于只读文件系统内 例7-4 一个简单聊天程序。

管道式单双工的,所以两个程序都需要建立相同的两个管道文件 操作步骤:

步骤1:编译源代码:

[root@localhost ~]vim 7-4zhang.c 程序源代码如下:

#include #include #include #include #include #include #include #include int main(){

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 #include #include #include #include #include #include #include int main(){

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\ rfd = open(\"fifo1\wfd = open(\"fifo2\if(rfd <= 0 || wfd <= 0)

return 0;

printf(\"这是李四端:\\n\"); while(1) {

FD_ZERO(&read_fd); FD_SET(rfd,&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)) { }

if(FD_ISSET(fileno(stdin),&read_fd)) { } }

close(rfd); close(wfd);

printf(\"-----------------------------------------------\\n\"); fgets(str,sizeof(str),stdin); len=write(wfd,str,strlen(str));

read(rfd,str,sizeof(str));

printf(\"-----------------------------------------------\\n\"); printf(\"张三:%s\\n\

步骤3:分别编译两个源文件:

[root@localhost ~]gcc -o 7-4zhang 7-4zhang.c [root@localhost ~]gcc -o 7-4li 7-4li.c

步骤4:在两个终端中分别执行7-4zhang和7-4li,得到如下结果:

我们使用有名管道实现了两个进程之间的通信!

小结:

管道常用于两个方面:

(1)在shell中时常会用到管道(作为输入输出的重定向),在这种应用方式下,管道的创建对于用户来说是透明的;

(2)用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。

FIFO可以说是管道的推广,克服了管道无名字的限制,使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信。管道和FIFO的数据是字节流,应用程序之间必须事先确定特定的传输\"协议\",采用传播具有特定意义的消息。

要灵活应用管道及FIFO,理解它们的读写规则是关键。 5.2信号

信号是UNIX中所使用的进程通信的一种最古老的方式,它是在软件层次上对中断机制的一种模拟,是一种异步通信方式。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间发生了哪些系统事件。它可以在任何时候发给某一进程,而无需知道该进程的状态。

如果该进程当前并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止,如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到该进程恢复被取消时才被传递。

5.2.1信号及信号来源

信号本质:

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

信号来源:

信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

当我们在命令行中执行命令“kill -l”就可以查看当前系统的所有支持的信

号,如图:

5.2.2、信号的种类

可以从两个不同的分类角度对信号进行分类: (1)可靠性方面:可靠信号与不可靠信号; (2)与时间的关系上:实时信号与非实时信号。

5.2.3.信号的生命周期

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:信号诞生;信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。相邻两个事件的时间间隔构成信号生命周期的一个阶段。

当一个可靠信号(实时信号)发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册);

当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做\"不可靠信号\"。这意味

着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构(一个非实时信号诞生后,(1)、如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失;(2)、如果进程的未决信号中没有相同信号,则在进程中注册自己)。

所有可靠的信号都支持排队,而不可靠的信号则不支持排队。

下面阐述四个事件的实际意义:

1.信号\"诞生\":信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()等)。

2.信号在目标进程中\"注册\":进程的task_struct结构(见附录B)中有关于本进程中未决信号的数据成员:

struct sigpending pending: struct sigpending{ struct sigqueue *head, **tail; }; sigset_t signal;

第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为\"未决信号信息链\")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

struct sigqueue{ struct sigqueue *next; siginfo_t info; }

信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。 (信号从产生到递达之间的状态,称为信号未决(Pending)。)

3.信号在进程中的注销:在进程执行过程中,会时刻检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最

多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不应该在进程的未决信号集中删除该信号(信号注销完毕)。

进程在执行信号相应处理函数之前,首先要把信号在进程中注销。

4.信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。

注意:信号的产生,注册,注销等都是指信号的内部实现机制,而不是信号的函数实现,因此信号注册与否,与后面的发送信号函数kill与信号安装函数signal无关。)

进程对信号的响应

进程可以通过三种方式来响应一个信号:

(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;

(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,Linux对每种信号都规定了默认操作。

下表列出了最为常见的信号含义及执行的其默认操作: 信号名 SIGHUP 含义 默认操作 该信号在用户终端连接结束时发出,通常是在终端的终止 控制进程结束时,通知同一会话内的各个作业与控制终端不再关联 SIGINT 该信号在用户键入INTR字符(通常是Ctrl-C)时发终止 出,终端驱动程序发送此信号并送到前台进程中的每一个进程 该信号和SIGINT类似,但由QUIT字符控制(通常终止 是Ctrl-\\) 该信号在一个进程企图执行一条非法指令时发出 终止 该信号在发生致命算数运算错误时发生,这里不仅包终止 括浮点运算错误,或者试图执行数据段,堆栈溢出时发生 该信号用来立即结束程序的运行,并且不能被阻塞、终止 处理和忽略。 SIGALRM 该信号当一个定时器到时的时候发出 SIGSTOP 终止 SIGQUIT SIGILL SIGFPE SIGKILL 该信号用于暂停一个进程,且不能被阻塞、处理或忽暂停进程 略 SIGTSTP 该信号用于交互停止进程,用户可键入SUSP字符时(通常是Ctrl+Z发出这个信号) 停止进程 忽略 SIGCHLD 子进程改变状态时,父进程会收到这个信号 进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。信号在内核中的表示可以看作是这样的: 信号在内核中的表示示意图

每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,

1. SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。 2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它

的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处

理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?

POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

5.2.4 信号的发送与捕捉:

内核如何实现信号的捕捉:

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

1. 用户程序注册了SIGQUIT信号的处理函数sighandler。

2. 当前正在执行main函数,这时发生中断或异常切换到内核态。 3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号

SIGQUIT递达。

4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而

是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次

进入内核态。

6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数

的上下文继续执行了。

图 信号的捕捉

发送信号的函数主要有kill(),raise(),alarm()以及pause(),下面就依次对其进行介绍:

kill系统调用说明: 所需文件头 #include #include 函数原型 函数功能 函数传入值 int kill(pid_t pid , int sig) 发送一个(指定的)信号给(指定的)进程 pid >0:要发送信号进程号 0:信号被发送到所有和pid进程在同一个进程组的进程 -1:信号发送给所有的进程表中的进程(除进程号最大的进程外) sig:信号 函数返回值: 成功:返回0; 失败:-1

raise系统调用说明: 所需文件头: 函数原型: 函数功能: 函数传入值: 函数返回值:

#include #include int raise(int sig) 将指定的信号向进程本身发送。 sig:信号 成功:0 失败:-1 注:sigqueue()系统调用说明:(一个新的系统调用,在不同的版本中有不同的实现,故不主要讲解) sigqueue()

#include #include

int sigqueue(pid_t pid, int sig, const union sigval val) 调用成功返回 0;否则,返回 -1。

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。

sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。 sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

typedef union sigval { int sival_int; void *sival_ptr; }sigval_t; 在调用sigqueue时,sigval_t指定的信息会拷贝到3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数; sigqueue()发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。

例7-5 父进程向子进程发送信号,结束子进程。 操作步骤:

步骤1:编译源文件: [root@localhost ~]vim 7-5.c 程序源代码如下:

#include #include #include #include #include #include int main(){

pid_t result; int ret; result = fork(); int newret; if(result < 0){

perror(\"创建子进程失败!\\n\"); exit(1);

}

else if(result == 0) { } else{

printf(\"子进程的进程号:%d\\n\

if((waitpid(result,NULL,WNOHANG))==0){ if((ret=kill(result,SIGKILL))==0) printf(\"用kill函数返回值是:%d,发送SIGKILL给子进程,执行raise(SIGSTOP);/*给自己发送一个信号,让自给暂停*/ exit(0);

SIGKILL信号的默认操作结束子进程:%d!\\n\ else{perror(\"kill函数结束子进程失败\");} }

}

}

步骤3:编译并执行程序,结果如下:

我们看到,子进程先发送一个SIGSTOP信号让自己暂停,然后父进程调用waitpid系统调用判断子进程是否被终止,如果没有被终止,则给子进程发送一个SIGKILL信号,然后打印语句。子进程接收到这个信号后,会执行这个信号的默认动作,终止自己。

alarm()和pause()

alarm 也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间倒时,它就向进程发送SIGALARM信号。要注意的是:一个进程只能有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。

pause函数适用于将调用进程挂起直到捕捉到信号为止。通常可以判断信号是否以到。

alarm()系统调用说明: 所需文件头 函数原型 函数传入值: 函数返回值: #include unsigned int alarm(unsigned int seconds) seconds:指定描述 成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0, 出错:-1

pause系统调用说明: 所需文件头 函数原型 函数返回值 #include int pause(void) -1,并且把error值设为EINTR

例:7-6 简单的alarm()与pause系统调用测试: 操作步骤:

步骤1:编译源文件:

[root@localhost ~]vim 7-6.c 程序源代码如下: #include #include #include int main(){ int ret; } ret=alarm(5); pause(); printf(\"我开始行动%d\\n\

步骤2:编译并运行程序,结果如下:

很有意思,是不是,进程并没有打印“我开始行动”这句话,而是打印了闹钟两字后终止,为什么呢?

因为进程在执行到“ret=alarm(5)”这句代码时,就会产生用于发射信号的闹钟,这个闹钟被设定为5秒的倒计时,然后程序继续执行下一句代码:“pause();

”此句代码的的效果是暂停进程,直到进程都到一个信号,那么进程在此句代码被执行后将被挂起,直到接收到一个信号为止。5秒钟后,计时器到时,闹钟被唤醒,向进程发送一个SIGALARM信号,进程此时正在被挂起,收到SIGALARM信号后恢复运行,进程收到信号就要执行信号的处理函数,上例中,并没有关于SIGALARM信号的处理函数,那么就要执行SIGALARM信号的默认动作,就是输出“闹钟”两字后,终止进程。所以在pause();语句后,进程收到信号继续执行,首先就要执行信号的处理函数,本例中信号处理函数的结果是结束进程,那么在还没有执行printf之前,进程被信号的默认动作终止。

5.2.5 信号(处理函数)的安装:

如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作(进程对信号的处理)之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。 linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,当然,sigaction()同样支持非实时信号的安装。

signal()函数

使用signal()函数处理时,只需要把要处理信号和处理函数列出即可。它主要要用于前32种非实时信号的处理,不支持信号传递信息,但是使用简单,易于理解。

所需文件头 函数原型 函数传入值 #include void (*signal(int signum, void (*handler)(int)))(int); signum:指定的信号 handler: SIG_IGN:忽略该信号 SIG_DFL:采用系统默认方式处理信号 函数返回值 自定义信号处理函数指针 成功:以前的信号处理配置 出错:-1 signal函数原型看似复杂,但是可以用如下typedef进行替换说明: typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler)); (注:typedef声明:为现有的类型创建一个新名字,例 typedef int size, 定义一个int的同义字,名字为size.)

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。

如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

例 7-7 signal函数的使用: 操作步骤:

步骤1:编译源文件: [root@localhost ~]vim 7-7.c 程序源代码如下:

#include #include #include

/*首先,定义一个属于自己的信号处理函数*/ void my_func(int sign_no) { }

if(sign_no==SIGINT) printf(\"我收到了一个SIGINT信号\\n\"); if(sign_no==SIGQUIT)

printf(\"我收到了一个SIGQUIT\\n\");

void my_func2(){ printf(\"aaaaaaaaaa\\n\"); }

int main() { printf(\"等待一个信号,SIGINT或SIGQUIT!\\n\"); }

signal(2,my_func); signal(3,my_func2); pause(); exit(0);

步骤2:编译并执行程序,结果如下:

运行程序,当我们按下键盘的“ctrl+\\”的时候,向进程发送了一个SIGQUIT信号,我们在进程中安装了SIGQUIT信号,并定义可SIGQUIT信号的处理函数‖my_func2()‖,它的动作是打印“aaaaaaaaaaa”,所以我们看到,当我们向进程发送SIGQUIT信号时候,程序的执行结果是在屏幕中输出了“aaaaaaaaaa”。然后进程终止。

我们再次运行程序,此时,按下键盘的“ctrl+c”,那么,就向进程发送了一个SIGINT信号,我们同样在进程安装了SIGINT信号,并定义了信号的处理函

数,因此,当进程收到SIGINT信号时,执行俄SIGINT信号的处理函数——即检查信号是什么信号,并把信号的名称输出。

5.2.6 信号集函数组

试想一下,可能会出现这样一种情况,有时,即不希望进程在接受到信号时立刻中断进程的执行,也不希望信号被完全忽视,而是希望延时一段时间再去调用信号处理函数,这时就需要信号集函数组(信号阻塞)来实现了。

信号函数组是一系列函数,按照调用的先后顺序可以分成几大功能模块,创建信号集合,登记信号处理器,以及检测信号。

创建信号集合——主要用于创建用户感兴趣的信号,其中,函数包括以下几个:

sigemptyset: 初始化信号集合为空;

sigfillset : 初始化信号集合为所有信号的集合; sigaddset : 将信号加入到信号集合中去。 sigdelset : 将信号从信号集中删去。

sigismember : 查询指定信号是否在信号集合之中。 登记信号处理器——就是决定进程如何处理信号。

信号集中的信号只有当信号的状态处于非阻塞状态时才真正起作用,(也就是说,阻塞状态下,信号集里面的信号将不可用),使用sigprocmask函数将信号集加入到阻塞当中。然后使用sigaction函数改变进程接到特定信号之后的行为。

3,检测信号——信号处理的后续步骤(不是必须的)。由于内核可以在任何时候向某一进程发出信号,因此,若该进程必须保持非中中断状态且希望将某些信号阻塞。这些信号就处于“未决”状态(相当于进程不清楚它的存在)所以,在希望保持非中断进程完成相应的任务后就应该将这些信号解除阻塞,sigpending函数允许进程检测这种“未决”信号,并进一步决定对他们作何处理。

函数格式: 所需文件头 函数原型 #include int sigemptyset(sigset_t *set) int sigfillset(sigset_t *set) int sigaddset(sigset_t *set,int signum) int sigdelset(sigset_t *set,int signum) 函数传入值 函数返回值 sigprocmask函数 所需文件头: 函数原型: #include int sigprocmask (int how , const sigset_t *set , sigset_t *oset) int sigismember(sigset_t *set,int signum) set: 信号集 signum:指定的信号 成功:0 失败:-1 (sigismember成功返回 1 ,失败返回 0) 函数传入值 how:决定SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合函数的操之中 作方式 SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合 SIG_SETMASK:将当前的信号集合设置为信号阻塞集合 set :把这个信号集合设置成新的当前进程的信号屏蔽字,如果为空,则不修改。 Oset :保存进程旧的信号屏蔽字,如果为空,则不保存 成功0,出错:-1 返回值 信号屏蔽字:信号屏蔽字就是进程中被阻塞的信号集,这些信号不能发送给进程,他们在该进程中被屏蔽了,实际上,他们是被阻塞了。 一个进程的信号屏蔽字实际上就是指这个进程被阻塞的信号集合!

Sigaction函数说明: 所需文件头 函数原型 函数传入值: #include int sigaction(int signum , const struct sigaction *act , struct sigaction *oldact) signum:信号的值,可以是(除了SIGKILL及SIGSTOP外的)任何一个特定有效信号 act: 指向sigaction的一个实例的指针,指针对特定信号的处理。 oldact: 保存原来对应信号的处理。 函数返回值:

Sigaction结构体: struct sigaction { }

void (*sa_handler) (int signo); sigset_t sa_mask; int sa_flags;

void (*sa_restore) (void);

成功:0,出错:-1 sa_handler是一个函数指针,指信号的关联函数,这里除了可以是用户自定义的处理函数外,还可以是SIG_DFL(采用缺省处理方式)或SIG_IGN(忽略信号),它的处理函数只有一个参数,及信号值。

sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。

sa_flags中包含了许多标志位,其意义如下: SA_NOSEFER、SA_NOMASK 当捕捉到信号时,执行起信号捕捉函数时,系统不会自动阻塞。 SA_NOCLDSTOP SA_RESTART 进程忽略子进程产生的任何SIGSTOP , SIGTSTP , SIGTTIN 和 SIGTTOU信号 可让重启的系统调用重新起作用 在执行完毕后恢复信号的系统SA_ONESHOT\\SA_RESETHAND 自定义信号只执行一次,默认动作

sigpending函数语法 所需文件头 函数原型 函数传入值: 函数返回值:

#include int sigpending(sigset_t *set) set: 要检测的信号集 成功 0 ,失败-1 函数有很多,看似复杂,实际上应用上并非如此难用,大致流程如下: 1>将需要的信号加入信号集(定义函数集) 2>设置信号的屏蔽位 3>定义信号处理函数 4>测试信号 列7-8

要求主体程序运行时,及时用户按下中断键(CTRL-C),也不能影响程序的正常运行,等主体运行完之后才进入自定义信号处理函数。

操作步骤: 步骤1:编及源文件:

[root@localhost ~]vim 7-8.c 程序源代码如下: #include #include #include #include #include void fun_ctrl_c () { } while(1){ } printf(\"如果你想退出,请发送SIGQUIT信号!\\n\"); sleep(2);

int main() { int i; sigset_t set, pendset; /*定义了两个信号集*/

struct sigaction action ;

(void)signal(SIGINT,fun_ctrl_c); /*捕捉到SIGINT信号时,调用fun_Ctrl_C函数*/ signal(SIGQUIT,SIG_DFL); if(sigemptyset(&set)<0) perror(\"初始化信号集合错误\");

if((sigaddset(&set,SIGINT) || sigaddset(&set,SIGQUIT))<0)

perror(\"将指定信号加如信号集失败\");

if(sigprocmask(SIG_BLOCK,&set,NULL)<0)/*把信号集合加入到当前进程的阻塞集合当中*/ else {

for(i=0;i<5;i++) { }

} if(sigprocmask(SIG_UNBLOCK,&set,NULL)<0)/*解除阻塞状态,当前阻塞集合中删除一个信号集合*/ }

perror(\"删除信号集的阻塞状态失败\");

printf(\"显视此文字,说明进程出于阻塞信号状态!\\n\"); sleep(2);

perror(\"往信号阻塞集增加一个信号集合错误\");

步骤2:编译并执行程序:

在程序中,信号SIGQUIT与信号SIGINT被设置进程的阻塞中,在程序开始运行以后的10秒钟里,发送信号无论是SIGQUIT还是SIGINT信号,信号都会被阻塞,10秒钟后,信号被提交给进程,进程接到信号,开始执行信号的处理函数。

列7-9设置两个信号到信号集,实现信号阻塞操作与自定义信号处理操作。

首先 将 SIGQUIT,与SIGINT两个信号加入到信号集,让后将该信号集设置为阻塞状态,并在该状态下是程序暂停5秒,接下来在将信号设置为非阻塞状态,再对信号分别操作,其中SIGQUIT执行默认操作,而SIGINT执行用户自定义的操作。 操作步骤:

步骤1:编辑源代码: [root@localhost ~]vim 7-8.c 源代码如下: #include #include #include #include #include void my_func(){ }

printf(\"如果你想退出,请发送SIGQUIT信号\\n\");

int main(){ sigset_t set,pendset;

struct sigaction action1,action2; if(sigemptyset(&set)<0)

perror(\"初始化清空信号集失败\"); if(sigaddset(&set,3)<0) perror(\"添加SIGQUIT信号到信号集失败\"); if(sigaddset(&set,2)<0) perror(\"添加SIGINT信号到信号集失败\"); if(sigprocmask(SIG_BLOCK,&set,NULL)<0) perror(\"将信号集设置为阻塞状态失败\"); else {

printf(\"信号集设置为阻塞状态成功\\n\"); sleep(5);

}

if(sigprocmask(SIG_UNBLOCK,&set,NULL)<0)

printf(\"将信号集设置为解除状态失败\\n\"); else printf(\"信号集解除阻塞成功!\\n\");

while(1)/*对相应的信号进行循环处理*/ }

{ }

if(sigismember(&set,SIGINT)){ sigemptyset(&action1.sa_mask); action1.sa_handler=my_func; }

sigaction(SIGINT,&action1,NULL);

else if(sigismember(&set,SIGQUIT)){ sigemptyset(&action2.sa_mask); action2.sa_handler = SIG_DFL; }

sigaction(SIGQUIT,&action2,NULL);

步骤2:编译并执行程序,结果如下:

5.3 消息队列:

“消息”是在计算机间传送的数据单位。消息可以非常简单,例如文本字符串;也可以更复杂,比如包含嵌入对象。

队列是信息的线性表,(线性表是最简单最常用的一种数据结构,例如:英文字母表(A,B,C,D„„,Z)就是一个线性表)它的访问次序是先进先出。

线性结构:有且只有一个开始点和一个终结点,并且所有节点都最多只有一个直接前趋和直接后继。

消息队列,就是一个消息的链表,(链表:采用链接方式存储线性表)是一系列保存在内核中的消息的列表。用户进程可以向消息队列添加消息,也可以从消息

队列读取消息,与管道通信非常相似。

消息队列,也叫做报文队列,它的出现克服了其他通信机制的一些缺点,比如信号,使用的信号的通信方式更像是“即时”的通信方式,它要求接受的信号的进程在某个时间范围内对信号做出反应,因此该信号最多在接受信号的进程的生命周期内才有意义,信号所传递的信息是接近于随进程持续的概念,而管道及有名管道则是典型的随进程持续的IPC,并且,只能传送无格式的字节流无疑会给应用程序开发带来不便,另外,它的缓冲区大小也受限制。

消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按队列次序,而是可以根据自定义条件接收特定类型的消息。

注:消息队列可以在无任何关系的进程间通信,这像命名管道FIFO,要比管道PIPE应用广泛;消息队列可以通过对列ID号,根据条件读取消息,这一点比管道优秀。

可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。

目前主要有两种类型的消息队列:POSIX消息队列以及系统V消息队列,系统V消息队列目前被大量使用。

5.3.1 消息队列的基本概念

1. system V消息队列是随内核持续的,只有在内核重起或者显示删除一个消息队列时,该消息队列才会真正被删除。因此系统中记录消息队列的数据结构(struct ipc_ids msg_ids)位于内核中,系统中的所有消息队列都可以在结构msg_ids中找到访问入口。

2. 消息队列就是一个消息的链表。每个消息队列都有一个队列头,用结构struct msg_queue来描述。队列头中包含了该消息队列的大量信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等等,甚至记录了最近对消息队列读写进程的ID。读者可以访问这些信息,也可以设置其中的某些信息。

3. 下图说明了内核与消息队列是怎样建立起联系的:

其中:struct ipc_ids msg_ids是内核中记录消息队列的全局数据结构;struct msg_queue是每个消息队列的队列头。

从上图可以看出,全局数据结构 struct ipc_ids msg_ids 可以访问到每个消息队列头的第一个成员:struct kern_ipc_perm;而每个struct kern_ipc_perm能够与具体的消息队列对应起来是因为在该结构中,有一个key_t类型成员key,而key则唯一确定一个消息队列。kern_ipc_perm结构如下:

struct kern_ipc_perm{ //内核中记录消息队列的全局数据结构msg_ids能够访问到该结构; key_t key; //该键值则唯一对应一个消息队列 uid_t uid; gid_t gid; uid_t cuid; gid_t cgid; mode_t mode; unsigned long seq; } 5.3.2、操作消息队列

对消息队列的操作无非有下面三种类型: 1、 打开或创建消息队列

消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,只需提供该消息队列的键值即可;

注:消息队列描述符是由在系统范围内唯一的键值生成的,而键值可以看作对应系统内的一条路经。

三种创建唯一键值的方法:ftok函数,key参数的为IPC_PRIVATE,直接指定。

2、 读写操作

消息读写操作非常简单,对开发人员来说,每个消息都类似如下的数据结构: struct msgbuf{ long mtype; char mtext[n]; }; mtype成员代表消息类型,从消息队列中读取消息的一个重要依据就是消息的类型;mtext是消息内容,当然长度不一定为1。因此,对于发送消息来说,首先预置一个msgbuf缓冲区并写入消息类型和内容,调用相应的发送函数即可;对读取消息来说,首先分配这样一个msgbuf缓冲区,然后把消息读入该缓冲区即可。

3、 获得或设置消息队列属性:

消息队列的信息基本上都保存在消息队列头中,因此,可以分配一个类似于消息队列头的结构(struct msqid_ds)来返回消息队列的属性;同样可以设置该数据结构。

5.3.3 消息队列API

要获得一个消息队列的描述字,我们需提供该消息队列的键值,而键值可以看作对应系统内的一条路经。

首先,从文件名到键值。 ftok函数: #include #include key_t ftok (char*pathname, char proj);

它返回与路径pathname相对应的一个键值(key)。该函数不直接对消息队列操作,但该KEY就是建立消息队列所使用的唯一键值,通过此键值,函数msgget就可以获得一个与该KEY相对应的消息队列描述符。

msgget函数:

打开(创建)消息队列 所需头文件: #include #include #include 函数功能: 函数原型: 函数传入值: 创建或打开消息队列 Key_t msgget (key_t key , int msgflg); Key: 唯一键值。(IPC_PRIVATE) Msgflg: IPC_CREAT、IPC_EXCL、IPC_NOWAIT或者是三者的或运算的结果 函数返回值: 执行成功返回消息队列描述符,出错 -1. Msgsnd()函数

添加消息到消息队列:

所需文件头: #include #include #include 函数功能: 函数原型 函数传入值: 函数返回值: 将消息添加到消息队列 int msgsnd (int msgid,struct msgbuf*msgp,int msgsze,int msgflg) 向msgid代表的消息队列发送一个消息,即将发送的消息存储在msgp指向的msgbuf结构中,消息的大小由msgze指定。Msgflg为标志 执行成功返回0,出错-1.

msgflg标志说明:

对发送消息来说,有意义的msgflg标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待。造成msgsnd()等待的条件有两种:

当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量; 当前消息队列的消息数(单位\"个\")不小于消息队列的总容量(单位\"字节数\"),此时,虽然消息队列中的消息数目很多,但基本上都只有一个字节。

msgsnd()解除阻塞的条件有三个:

1. 不满足上述两个条件,即消息队列中有容纳该消息的空间; 2. msqid代表的消息队列被删除; 3. 调用msgsnd()的进程被信号中断;

从消息队列中读取消息: Msgrcv()函数:

所需文件头: #include #include #include 函数功能: 函数原型: 参数说明: 从消息队列中读取消息 int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg); 从msgid代表的消息队列中读取一个消息,并把消息存储在msgp指向的msgbuf结构中。 Msgsz:消息的数据长度 Msgtyp:用来指定消息的种类。 =0,返回队列的第一条消息; >0,返回队列内第一项msgtyp与mtype匹配的消息。 <0,返回队列内地第一项Mtype小于或等于msgtyp绝对值的消息。 Msgflg标志 函数返回值: 成功0,失败:-1. msgflg表示说明:

标志msgflg可以为以下几个常值的或运算:

IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,此时,errno=ENOMSG

IPC_EXCEPT 与msgtyp>0配合使用,返回队列中第一个类型不为msgtyp的消息

IPC_NOERROR 如果队列中满足条件的消息内容大于所请求的msgsz字节,则把该消息截断,截断部分将丢失

msgctl函数

int msgctl(int msqid, int cmd, struct msqid_ds *buf); 对由msqid标识的消息队列执行cmd(命令)操作。 共有三种cmd操作:

IPC_STAT、IPC_SET 、IPC_RMID。

1.>IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid结构中;

2. >IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid结构中;可设置属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes,同时,也影响msg_ctime成员。

3. >IPC_RMID:删除msqid标识的消息队列;

调用返回:成功返回0,否则返回-1。

例8-1,利用消息队列实现进程间的通信: 操作步骤:

步骤1:编译创建队列并添加消息的源代码:

[root@localhost ~]vim 8-1a.c 程序源代码如下;

#include #include #include #include #include #include #include

struct msgnbuf {

long msg_type;

char msg_text[512];

};

int main(){ int qid,len;

key_t key;

struct msgnbuf msg;

if((key=ftok(\".\调用函数,产生标准唯一键值*/ { }

perror(\"产生标准KEY出错!\"); exit(1);

if((qid=msgget(key,IPC_CREAT|0666))==-1) /*调用函数,打开一个与KEY相对应的的消息队列,如果消息队列不存在就创建它,并付于它足够的权限*/ */ }

{

perror(\"创建消息队列出错\");

exit(1); }

printf(\"已经打开(创建)了一个消息队列,队列描述符是%d:\puts(\"请输入要加如消息!\");

if((fgets(msg.msg_text,512,stdin))==NULL) { }

puts(\"没有消息\"); exit(1);

msg.msg_type=1;

len = strlen(msg.msg_text);

if(msgsnd(qid,&msg,len,IPC_NOWAIT)<0) /*调用msgsnd函数,添加消息到消息队列{

perror(\"添加消息到消息队列失败.\"); exit(1);

} else

printf(\"向消息队列添加消息成功!\\n\");

步骤2:编辑读取消息的源代码: [root@localhost ~]vim 8-1b.c 程序源代码如下:

#include #include #include #include #include #include #include

struct msgbuf {

long msg_type;

char msg_text[512];

};

int main(){ }

int qid,len;

key_t key;

struct msgbuf msg;

if((key=ftok(\".\{

perror(\"产生标准KEY出错!\");

exit(1); }

if((qid=msgget(key,IPC_CREAT|0666))==-1) { }

perror(\"打开消息队列出错\"); exit(1);

printf(\"打开消息队列%d成功,现在从消息队列中读取消息!\if((msgrcv(qid,&msg,512,1,IPC_NOWAIT))<0) {

perror(\"读取消息出错\"); exit(1);

} else

printf(\"读取的消息是:%s\\n\

步骤3:编译并执行程序,结果如下:

8-1a创建消息队列,并向队列添加消息:

当我们执行“8-1a”时候,进程创建了一个消息队列,并要求我们向消息队列中添加消息,这时,我们输入:“你好,测试消息队列”。回车后,进程将我们键入消息写进消息队列,并向屏幕输出“想消息队列添加消息成功”。

执行8-1b读取消息,结果如下:

8-1b进程将另一个进程8-1a键入的消息读出! 5.4 共享内存

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。一般而言,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

Linux的2.2.x以上内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存,但是linux发行版本如(Redhat 8.0/9.0)支持mmap()系统调用及系统V共享内存,并没实现Posix共享内存,我们将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。

5.4.1 系统调用共享内存 mmap系统调用

系统调用mmap()通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

mmap()系统调用形式如下:

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

参数addr: 指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,

此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。

len参数:是映射到调用进程地址空间的字节数,它从被映射文件开头offset个

字节开始算起。

prot 参数: 指定共享内存的访问权限。可取如下几个值的或:PROT_READ

(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。

Flags参数:由以下几个常值指定:MAP_SHARED (对应区域的写入数据会

复制回文件内,而且允许其他映射该文件的进程共享), MAP_PRIVATE(对应区域的写入数据会复制回文件内,但是不允许其他映射该文件的进程共享) , MAP_FIXED(如果参数start所指的地址无法成功建立映射,则放弃映射,不对地址做修正,通常不鼓励用此旗标。),其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。

fd参数:为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd

可以指定为-1,此时须指定flags参数中的MAP_ANONYMOUS,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。

offset参数:一般设为0,表示从文件头开始映射。 所需文件头:#include

#include

系统调用mmap()用于共享内存的两种方式:

(1)使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:

fd=open(name, flag, mode); if(fd>0) ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); ...

(2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。

对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可.(fd :-1,flag MAP_ANONYMOUS )

系统调用munmap()

int munmap( void * addr, size_t len )

该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

例8-2,两个进程通过映射普通文件实现共享内存通信 操作步骤:

步骤1:编辑源代码:8-2file1.c [root@localhost ~]vim 8-2file1.c 程序源代码如下: #include #include #include #include typedef struct{ char name[4]; int age; }people; int main(){

int fd,i;

people *p_map; char temp;

fd=open(\"/tmp/memrey\lseek(fd,sizeof(people)*5-1,SEEK_SET); write(fd,\"\

p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); close(fd); }

temp = 'a'; for(i=0;i<10;i++) { }

printf(\"向共享内存添加数据成功!\\n\"); sleep(20);

munmap(p_map,sizeof(people)*10); printf(\"关闭共享内存映射!\\n\");

temp +=1;

memcpy((*(p_map+i)).name,&temp,2); (*(p_map+i)).age = 20 +i;

8-2file1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采用结构的方式有普遍代表性)。8-2file1首先打开或创建一个文件(/tmp/memrey),并把文件的长度设置为5个people结构大小。然后从mmap()的返回地址开始,设置了10个people结构。然后,进程睡眠20秒钟,等待其他进程映射同一个文件,最后解除映射。

步骤2:编辑源代码8-2file2.c [root@localhost ~]vim 8-1b.c 程序源代码如下: #include #include #include #include typedef struct{ char name[4]; int age; }people; main(){ int fd,i; people *p_map; fd=open(\"/tmp/memrey\ p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); } for(i = 0;i<10;i++) { printf(\"name: %s age: %d;\\n\} munmap(p_map,sizeof(people)*10); 8-2file2.c只是映射一个文件到共享内存,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。 分别把两个程序编译成可执行文件8-1file1和8-2file2后,在一个终端上先运行./8-1file1,程序输出结果如下:

在8-2file1输出“向共享内存添加数据成功”之后,输出“关闭共享内存映射”之前,在另一个终端上运行8-2file2,将会产生如下输出结果:

在8-2file1 输出“输出关闭共享内存映射”后,运行8-2file2则输出如下结果:

从程序的运行结果中可以得出的结论

1、 最终被映射文件的内容(共享内存的大小)的长度不会超过文件本身的初始大小,即映射不能改变文件的大小;

2、 可用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。文件被为5个people结构大小,而在8-1lfile1中初始化了10个people数据结构,在恰当时候(没有解除映射之前)调用8-2lfile2会发现8-2file2将输出全部10个people结构的值。

注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程可以对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;但是,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。

3、 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,在调用了munmap()后,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

例8-3,父子进程之间通过匿名映射实现共享内存。

操作步骤:

步骤1:编译源文件 [root@localhost ~]vim 8-1b.c 程序源代码如下: #include #include #include #include typedef struct{ char name[4]; int age; }people; main(){

int i,j;

people *p_map;

char temp; p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0); }

if(fork() == 0) {

sleep(2);

for(i=0;i<5;i++) printf(\"子进程从共享内存读数据:第%d个people的name是:%s , agefor(j=0;j<5;j++)

(*(p_map+j)).age = 100+j;

是: %d\\n\

munmap(p_map,sizeof(people)*10); exit(1); }

temp = 'a';

for(i=0;i<5;i++){ temp +=1;

memcpy((*(p_map+i)).name, &temp,2); (*(p_map+i)).age=20+i; }

sleep(8);

for(j=0;j<5;j++)

printf(\"父进程读:第%d个people的age是:%d\\n\printf(\"准备解除映射\\n\");

munmap(p_map,sizeof(people)*10); printf(\"解除成功\\n\");

步骤2:编译并执行程序,结果如下:

5.4.2 系统V共享内存:

系统V则是通过映射特殊文件系统shm( shmfs 文件系统表示是根据 Linux 2.4 中的 POSIX 标准,挂载为特殊文件系统的共享内存文件系统。)中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特殊文件系统shm中的一个文件。 系统V共享内存原理

进程间需要共享的数据被放在一个叫做IPC共享内存区的地方,所有需要访问该共享区域的进程都需要把该共享区域映射到本进程的地址空间中去。也就是说,任何想访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。 实现方法

系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。

所需文件头 函数功能: 函数原型: 函数传入值: #include #include 获取共享内存区域的ID int shmget(key_t key ,int size,int shmflg); Key,唯一键值; Size: 共享内存大小。 Shmflg: 标志。 执行成功则返回共享内存标识符,出错-1. 函数返回值:

然后,需要通过shmat映射到具体进程。 所需文件头: 函数功能: 函数原型: 函数传入值: #include #include 映射共享内存 Void *shmat(int shmid,const void *shmaddr,int shmflg); Shmid : 共享内存标识符 Shmaddr: 共享内存要映射的地址;0则表示把该内存映射到调用进程的地址空间. shmflg 函数返回值:

shmctl函数

与信号量那复杂的函数相比,共享内存的控制函数要简单得多。下面是定义: int shmctl(int shm_id , int command , struct shmid_ds *buf); shmid_ds 结构至少应该包含以下几个成员: struct shmid_ds {

uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode;

SHM_RDONLY 共享内存只读 默认0:共享内存可读写。 执行成功返回被映射的地址,不成功返回-1. }

第一个参数shm_id是由shmget函数返回的共享内存标识码。 第二个参数command 是将要采取的动作,它有三个可取值:

IPC_STAT : 把shmid_ds结构中的数据设置为共享内存当前关联值

IPC_SET : 在进程有足够权限的前提下,把共享内存的当前关值设置为shmid_ds

数据结构中给出的值

IPC_RMID :删除共享内存段

第三个参数buf 是一个指针,它指向一个保存着共享内存的模式状态和访问权限的数据结构。

成功返回“0”,失败返回“-1”。

最后解除共享地址的映射 所需文件头: #include #include #include int shmdt(const void *shmaddr) Shmaddr:被映射的共享内存段地址 成功: 0 出错:-1 函数原型: 函数传入值: 函数返回值:

列:8-4,使用系统V实现进程间的通信。 操作步骤:

步骤1:编译源文件8-4a.c [root@localhost ~]vim 8-4a.c 程序源代码如下:

#include #include #include #include typedef struct{ char name[4]; int age; }people; main(){

int shm_id,i; key_t key; char temp;

people *p_map; // char* name=\"./myshm\"; // key = ftok(name,'a'); if(key==-1)

perror(\"创建唯一建值错误\\n\");

shm_id=shmget((key_t)12345,4096,IPC_CREAT); if(shm_id==-1) {

perror(\"创建共享内存失败\\n\"); return; }

p_map=(people*)shmat(shm_id,0,0); temp ='a';

for(i=0;i<10;i++){ temp+=1;

memcpy((*(p_map+i)).name,&temp,1); (*(p_map+i)).age=20+i; }

if(shmdt(p_map)==-1) perror(\"解除映射出错\\n\"); }

步骤2:编译8-4b.c [root@localhost ~]vim 8-4b.c 程序源代码如下: #include #include #include #include #include #include typedef struct{ char name[4]; int age; }people; main(){

int shm_id,i;

key_t key;

people *p_map;

// char* name = \"./myshm\"; // key = ftok(name,'b'); // if(key == -1)

// perror(\"唯一键值\\n\");

shm_id = shmget((key_t)12345,4096,IPC_CREAT); if(shm_id == -1) {

perror(\"共享内存创建失败\\n\"); }

p_map=(people*)shmat(shm_id,0,0); if (p_map == (void *)-1) {

fprintf(stderr, \"shmat failed\\n\"); //exit(EXIT_FAILURE); }

for(i = 0;i<10;i++) {

printf(\"name: %s age: %d;\\n\}

if(shmdt(p_map)==-1)

perror(\"解除映射出错\\n\"); }

步骤3:编译并执行程序,结果如下:

执行8-4a 建立共享内存,并添加内容,执行8-4b,则将共享内存区域的数据打印出来。

第六章 多线程编程

前面我们讲过进程,进程是程序执行的过程,是一个基本动态的概念,它是系统中程序执行和资源分配的基本单位,那么,对于进程来讲,有两件事要做,1.程序执行,2,资源分配。我们把这两件事中的其中之一的“程序执行”单独拿出来,演化出一个新的概念——线程。

这就有了教科书上的定义:进程是资源管理的最小单位,线程是进程中程序执行的最小单位。

为什么会衍生出线程的概念?

随着计算机的发展,性能在不断的增强,资源以及数据同时的不断扩大,要处理的问题也越来越复杂,X86架构的硬件已经成为瓶颈,我们知道在这种架构上CPU的主频最高为4G,而目前,3.6G,3.8G的主频已经接近了颠峰。

短期内,从根本上解决这种架构的CPU瓶颈问题来满足社会需求基本不太现实(可能会需要很长时间以及大量的成本)那么,为了继续提高CPU的性能就需要在现有基础上尽量节约资源的调度,减少开销。于是超线程CPU模式的概念就衍生而来。各种系统作业,应用程序要减少处理开销,发挥CPU的最大功效就是改变到以多线程编程模型为主的处理系统和并发应用程序。

例如:

第一,当我们进行进程和进程之间的切换时,造成了CPU对资源管理的大量开销。而在进程中,线程是程序的执行最小单位,所有属于这个进程中的线程都共享一个进程的资源分配,在线程之间的切换,不会增加处理器的额外的负担。

第二,线程是进程中程序执行的最小单位,那么一个进程至少需要一个线程为它做指令执行,进程管理着资源(比如CPU,内存,文件等),而将线程分配到CPU上执行,一个进程当然可以拥有多个线程,此时,如果进程是运行在多处理器的机器上,它就可以采用多个CPU来执行各个线程,达到最大程度的并行,提高的效率。

从上面两种提高CPU的性能上,又衍生出了两种线程的概念,用户级线程以及核心级线程。主要区分它们的标准是线程的调度者是在核内还是在核外,核外是用户级线程,核内是核心级线程,前者更多的是考虑上下文切换的开销,而后者更利于并发使用多处理器资源,而在实际应用中,通常是将两者结合起来。既要减少上下文切换的开销,也就发挥多处理的功效,此时,一个核心线程可能同时被多个用户态线程调度,这就所谓的“多对一”,或者一个核心级线程就被一个用户态线程调度使用,那就叫“一对一”。 6.1 Linux线程的发展:

早在LINUX2.2内核中。并不存在真正意义上的线程,当时Linux中常用的

线程pthread实际上是通过进程来模拟的,也就是同过fork来创建“轻”进程,并且这种轻进程的线程也有个数的限制:最多只能有4096和此类线程同时运行。.

2.4内核消除了个数上的限制,并且允许在系统运行中动态的调整进程数的上限,当时采用的是Linux Thread 线程库,它对应的线程模型是“一对一”,而线程的管理是在内核为的函数库中实现,这种线程得到了广泛的应用。但是它不与POSIX兼容。另外还有许多诸如信号处理,进程ID等方面的问题没有完全解决。

相似新的2.6内核中,进程调度通过重新的编写,删除了以前版本中的效率不高的算法,内核框架页也被重新编写。开始使用NPTL(Native POSIX Thread Library)线程库,这个线程库有以下几个目标:

POSIX兼容,都处理结果和应用,底启动开销,低链接开销,与Linux Thread应用的二进制兼容,软硬件的可扩展能力,与C++集成等。

这一切是2.6的内核多线程机制更加完备。

6.2 Linux 线程的实现 Linux线程的基本操作

这里主要讲的线程以及相关操作都是用户空间的线程操作,在Linux中,一般pthread线程库是一套通用的线程库,是由POSIX提出的,因此具有很好的可移植性。

线程的创建和退出:

创建线程通常使用的函数是pthread_create.在线程创建以后,就开始运行相关的线程函数,在该函数运行完之后,该线程也就退出了。这是线程退出的一种方法: 运行完毕,自动退出。另一种是调用pthread_exit函数主动退出。最后,进程终止函数exit函数,一旦结束了进程,那么此进程中所有线程都将无条件终止。

一个注意点:在默认线程属性下,如果一个进程有很多线程在同时运行,一个线程在退出以后,当前线程所占用的资源并不会随着线程的终止而得到释放。因为所有处在一个进程中的线程共享资源。

线程中还有一个常用函数:pthread_join函数可以用于将当前线程挂起,等待其他线程结束。实际上,这个函数是就是一个线程阻塞函数,调用它的函数将一直等待到被等待的线程结束为止。当函数返回时,被等待线程的资源就被回收。

pthread_creat 函数: 所需文件头: #include 函数原型: int pthread_cread((pthread_t*thread,pthread_attr_r*attr,void*(*start_routine) (void*),void *arg)) 函数传入值: thread:线程标识符 attr: 线程属性设置 null表示采用默认 start_roitine : 线程函数的启示地址 arg :传递给start_routine的参数 函数返回值: 成功:0 出错:-1

pthread_exit函数

所需文件头: #include 函数原型: Void pthread_exit(void *retval) 函数传入值: retval:调用者线程的返回值,可由其他函数如pthread_join来检索获取。 pthread_join函数 所需文件头: #include 函数原型: int pthread_join ((pthread_t th,void **thread_return)) 函数传入值: th: 等待线程的标识符 thread_return:用户定义的指针,用来存储被等待线程的返回值(不为NULL时) 函数返回值: 成功:0

出错:-1 例:多线程使用例子:9-1 操作步骤:

步骤1:编译代码: [root@localhost ~]vim 9-1.c 程序源代码如下: #include #include void thread1(){ int i; for(i=0;i<6;i++){ printf(\"这是线程thread1!\\n\"); } if(i==2) pthread_exit(0); } void thread2(){ int i; for(i=0;i<3;i++)

printf(\"这是线程thread2!\\n\"); pthread_exit(0); }

int main(){ }

pthread_t id1,id2; int i,ret;

ret=pthread_create(&id1,NULL,(void *)thread1,NULL); if(ret!=0){ printf (\"创建线程失败1\\n\"); }

ret=pthread_create(&id2,NULL,(void *)thread2,NULL); if(ret!=0){ }

pthread_join(id1,NULL); pthread_join(id2,NULL); exit(0);

printf(\"创建线程2失败\\n\"); exit(1); exit(1);

步骤2:编译并运行程序,结果如下:

6.3 线程的属性

在pthread_creat函数中的第二个参数是“线程属性”。在上一个实例中,我们把该参数设置为NULL,也就是采用默认属性,其实,线程的属性是可以修改的,这些属性包括绑定属性,分离属性,地址堆栈,优先级。其中,系统默认属性是:非绑定,非分离,缺省1M的堆栈,与父进程同样级别的优先级。

绑定属性:

前面提到,线程分为两种线程,一个完整的线程在被调度时在两个空间中各自有属于自己空间的状态,在用户空间运行的是用户态线程,在内核空间运行的是核心态线程。其中有两种机制,一对一和多对一,一对一就是一个用户线程对应一个内核线程,绑定属性就是指一个用户级线程固定的分配给一个内核线程。绑定属性可以保证在需要的时候总有一个内核线程与之对应。而非绑定就是指用户线程和内核线程的关系不是始终固定的,而是由系统来控制分配的。

分离属性:

分离属性是用来决定一个线程以什么样的方式来终止自己。在非分离情况下,当一个线程结束时,它所占用的系统资源并没有被释放,也就是没有真正终止。只有当pthread_join函数返回时,创建的线程才释放自己占用的系统资源。在分离情况下,一个线程结束后立即释放它所占用的系统资源。真正意义上的线程结束。但是有一点要注意,当一个线程的属性被设置为分离时,而这个线程任务不重并且运行的非常快,那么它很可能在pthread_creat函数返回前就终止,这种终止时真正的结束,那么它结束以后,就可能将线程号和系统资源移交给其他线程使用。这是调用pthread_creat的线程就将得到错误的线程号。

线程属性的设置:

关于线程的属性设置都是通过一定的函数来完成的,通常首先调用设置绑定属性的函数为pthread_attr_setscope,设置线分离属性的函数为:pthread_attr_init函数进行初始化,之后再调用相应的属性设置函数。

pthread_attr_setdetachstate,设置线程优先级的相关函数为pthread_attr_getschedparam(获得线程的优先级)和pthread_attr_setschedparam(设置线程优先级),在设置完这些属性之后,就可以调用pthread_create函数来创建线程了。

pthread_attr_init 所需文件头: 函数原型: 函数传入值: 函数功能: 函数返回值: 所需文件头: 函数原型: 函数传入值: #include int pthread_arrt_setscope(pthread_attr_t *attr,int scope) attr:线程属性 Scope: PTHREAD_SCOPE_SYSTEM : 绑定 #include int pthread_arrt_init(pthread_attr_t *attr) attr:线程属性 (输出参数) 初始化线程 成功:0 出错:-1 PTHREAD_SCOPE_PROCESS : 非绑定 函数功能: 函数返回值: 所需文件头: 函数原型: 函数传入值: #include int pthread_arrt_setscope(pthread_attr_t *attr,int detachstate) attr:线程属性 Detachstate: PTHREAD_CREATE_DETACHED : 分离 函数功能: 函数返回值: 所需文件头: 函数原型: 函数传入值: 函数功能: 函数返回值: 所需文件头: 函数原型: 函数传入值: 函数功能: 函数返回值: #include int pthread_arrt_setschedparam(pthread_attr_t sched_param *param) attr:线程属性 param :线程优先级 (输出参数) 设置线程的优先级 成功:0 出错:-1 *attr,struct #include int pthread_arrt_getschedparam(pthread_attr_t *attr,struct sched_param *param) attr:线程属性 param :线程优先级 (输出参数) 获取线程的优先级 成功:0 出错:-1 PTHREAD_CREATE_JOINABLE : 非分离 设置线程的分离属性 成功:0 出错:-1 设置线程的绑定属性。 成功:0 出错:-1 例:9-2通过设置分离实行查看内存使用状态。 程序源代码如下: #include #include #include void thread1(void) { int i=0 ; for(i=0;i<10;i++){ printf(\"this is a pthread1.\\n\"); } if(i==8){ pthread_exit(0); } sleep(2); }

void thread2(void) { int i ; while(1){ }

int main(){ pthread_t id1,id2; }

pthread_attr_init(&attr);

pthread_attr_setscope(&attr,PTHREAD_SCOPE_SYSTEM);

pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); ret = pthread_create(&id1,&attr,(void *)thread1,NULL); if (ret!=0){ printf(\"创建线程1错误!\\n\"); exit(1); }

ret = pthread_create(&id2,NULL,(void *)thread2,NULL); if(ret!=0){ printf(\"创建线程2错误!\\n\"); exit(1); }

pthread_join(id2,NULL); return (0);

int i,ret;

pthread_attr_t attr; //定义线程属性变量

for(i=0;i<3;i++) printf(\"This is phread2.\\n\");

sleep(1); }

pthread_exit(0);

6.4 线程的访问控制

线程的访问控制就是指实现线程的同步和互斥。

线程的同步是指线程间的协同工作(一个生产,一个消费。一个线程生产一

个,另一个线程就只能消费一个。)互斥是指线程共享进程的资源时,对资源的上锁行为,在同一个时间保证只有一个线程使用资源。

线程间的互斥:

由于线程共享进程的资源和地址空间,因此在对这些资源进行操作时,必须

要考虑到线程间资源访问的唯一性问题,POSIX提供给我们两种方法:互斥锁和信号量

互斥锁:

Mutex 是一种简单的加锁的方法来控制对共享资源的存取。这个互斥锁只有两行状态,上锁和解锁。可以把这种互斥锁看成是某种全局变量,在同一时间只有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作,若其他线程希望上锁一个已经上了锁的互斥锁。该线程就会挂起。直到上锁定线程释放掉互斥锁为止。

可以这么理解,互斥锁使共享资源按照一定的顺序在线程中被使用。 互斥锁操作主要包括以下几个步骤:

1, 互斥锁初始化:pthread_mutex_init 2, 互斥锁上锁: pthread_mutex_lock 3, 互斥锁判断上锁:pthread_mutex_trylock 4, 互斥锁解锁:pthread_mutex_unlock 5, 消除互斥锁:pthread_mutex_destroy

其中,互斥锁可以分为快速互斥锁,递归互斥锁和检错互斥锁,这三种锁的区别主要在于其他未占有互斥锁的线程在希望得到互斥锁时的是否需要阻塞等待。快速锁是指调用线程会阻塞直到拥有互斥锁的线程解锁为止。检错互斥锁则为快速锁的非阻塞版本,它会立即返回并返回一个错误消息。递归互斥锁能够成功返回并增加调用线程在互斥上加锁的次数。

Pthread_mutex_init 所需文件头: #include 函数原型: int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr) 函数传入值: Mutex :互斥锁 Mutexattr: PTHREAD_MUTEX_INITIALIZER 创建快速互斥锁 PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP 创建递归互斥锁 PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP 创建检错互斥锁 函数返回值 所需文件头: 函数原型: #include int pthread_mutex_lock(pthread_mutex_t *mutex)互斥锁上锁 成功 0,出错 -1 int pthread_mutex_trylock(pthread_mutex_t *mutex) 互斥锁判断上锁 int pthread_mutex_unlock(pthread_mutex_t *mutex) 互斥锁解锁 int pthread_mutex_destroy(pthread_mutex_t *mutex) 消除互斥锁 函数传入值: 函数返回值 Mutex:互斥锁 成功 0,出错 -1 例 9-3 多线程互斥锁操作 操作步骤:

步骤1:编辑源代码: [root@localhost ~]vim 9-3.c 程序源代码如下: #include #include #include #include #include #include

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int lock_var; time_t end_time;

void pthread1(void *arg); void pthread2(void *arg);

int main(int argc,char *argv[]) { }

pthread_t id1,id2; pthread_t mon_th_id; int ret;

end_time = time(NULL) +10;

pthread_mutex_init(&mutex,NULL);

ret = pthread_create(&id1,NULL,(void *)pthread1,NULL); if(ret!=0)

perror(\"创建线程1错误!\\n\");

ret = pthread_create(&id2,NULL,(void *)pthread2,NULL); if(ret!=0) perror(\"创建线程2错误\\n\"); pthread_join(id1,NULL); pthread_join(id2,NULL); exit(0);

void pthread1(void *arg){

int i;

while(time(NULL) < end_time){

if (pthread_mutex_lock(&mutex)!=0){ perror(\"线程1上锁失败!\\n\"); }

else

printf(\"线程1现在已经将资源上锁\\n\"); for(i=0;i<2;i++){ sleep(1); }

lock_var++;

if(pthread_mutex_unlock(&mutex)!=0){ perror(\"线程1解锁失败!\\n\"); } else }

printf(\"线程1解锁成功\\n\"); sleep(1);

}

void pthread2(void *arg){ } }

int nolock=0; int ret;

while(time(NULL)if(ret==EBUSY){

printf(\"检查到线程1已经对资源上锁!\\n\"); } else{ if(ret!=0){

perror(\"尝试上锁失败!\\n\"); exit(1);

} else

printf(\"线程2开始上锁,上锁的时候lock_var的值是:%d\\n\if(pthread_mutex_unlock(&mutex)!=0){ perror(\"线程2解锁失败!\\n\"); } else }

printf(\"线程2解锁!\\n\");

sleep(3);

步骤2:编译并执行程序,结果如下:

6.5 多线程的条件变量:

条件变量cond与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量可以使线程睡眠以等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程为等待\"条件变量的条件成立\"而挂起;另一个线程使\"条件成立\"(给出条件成立信号)。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。 使用条件变量之前要先进行初始化。可以在单个语句中生成和初始化一个条件变量如:

pthread_cond_t my_condition=PTHREAD_COND_INITIALIZER; 名称: 目标: 头文件: 函数原形: 参数: pthread_cond_init 条件变量初始化 #include < pthread.h> int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); cond 条件变量指针 attr 条件变量属性可以用它来设置条件变量是进程内可用还是进程间可用,默认值是 PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。 NULL表示使用默认属性。 返回值: 成功返回0,出错返回错误编号。

pthread_cond_destroy函数可以用来摧毁所指定的条件变量,同时将会释放所给它分配的资源。 名称: 目标: 头文件: 函数原形: 参数: 返回值: 名称: 目标: 头文件: 函数原形: pthread_cond_wait/pthread_cond_timedwait 解开mutex指向的锁并被条件变量cond阻塞 #include < pthread.h> int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t 参数: 返回值:

mytex,const struct timespec *abstime); cond 条件变量 mutex 互斥锁 成功返回0,出错返回错误编号。 pthread_cond_destroy 条件变量消除 #include < pthread.h> int pthread_cond_destroy(pthread_cond_t *cond); cond 条件变量 成功返回0,出错返回错误编号。 第一个参数*cond是指向一个条件变量的指针。第二个参数*mutex则是对相关的互斥锁的指针。函数pthread_cond_timedwait函数类型与函数

pthread_cond_wait,区别在于,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。并返回错误ETIME,pthread_cond_timedwait函数的参数*abstime指向一个timespec结构。该结构如下:

typedef struct timespec{ time_t tv_sec; long tv_nsex; }timespec_t; 名称: 目标: 头文件: pthread_cond_signal/pthread_cond_broadcast 条件变量通知 #include < pthread.h> 函数原形: 参数: 返回值:

int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond); cond 条件变量 成功返回0,出错返回错误编号。 参数*cond是对类型为pthread_cond_t 的一个条件变量的指针。当调用pthread_cond_signal时一个在相同条件变量上阻塞的线程将被唤醒。如果同时有多个线程阻塞,则由调度策略确定接收通知的线程。如果调用pthread_cond_broadcast,则将通知阻塞在这个条件变量上的所有线程。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。如果当前没有线程等待通知,则上面两种调用实际上成为一个空操作。如果参数*cond指向非法地址,则返回值EINVAL。

下面是一个简单的例子,我们可以从程序的运行来了解条件变量的作用:

例 9-4 多线程条件变量的使用: 操作步骤:

步骤1:编辑源代码:

[root@localhost ~]vim 9-4.c 程序源代码如下:

#include #include #include

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; //初始化互斥锁 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //初始化条件变量

void *thread1(void *); void *thread2(void *);

int i = 1; int main(void){

pthread_t id1; pthread_t id2;

pthread_create(&id1,NULL,thread2,NULL); pthread_create(&id2,NULL,thread1,NULL);

pthread_join(id2,NULL);//等待线程2结束 pthread_mutex_destroy(&mutex);

}

pthread_cond_destroy(&cond); exit(0);

void *thread1(void *a){ }

void *thread2(void *a){ }

while(i<9){ }

pthread_mutex_lock(&mutex); if(i%3!=0)

pthread_cond_wait(&cond,&mutex); printf(\"thread2:%d\\n\

pthread_mutex_unlock(&mutex); sleep(1); for(i=1;i<=9;i++){ }

pthread_mutex_lock(&mutex); if(i%3==0)

pthread_cond_signal(&cond); //如果i是3的倍数,发送信号唤醒所有使用else

printf(\"thread1:%d\\n\

条件变量等待的线程

pthread_mutex_unlock(&mutex); sleep(1);

程序创建了2个新线程使他们同步运行,实现进程id2打印20以内3的倍数,id1打印其他的数,程序开始线程id2不满足条件等待,线程id1运行使a循环加1并打印。直到i为3的倍数时,线程id1发送信号通知进程id2,这时id2满足条件,打印i值。 下面是运行结果: thread1:1 thread1:2 thread2:3 thread1:4 thread1:5 thread2:6 thread1:7 thread1:8 thread2:9

信号量线程控制 1. 什么是信号量?

信号量本质是上一个非负的整数计数器,它被用来控制对公共资源的访问。 2. PV原语?

PV原语是对整数计数器信号量sem的操作,一次P操作使sem减一,而一次V操作使sem加一,进程(或线程)根据信号量的值来判断是否可对公共资源具有访问权。当信号量sem的值大于等于零时,该进程(或线程)就具有对公共资源的访问权,如果sem的值小于零时,该进程(或线程)就一直阻塞直到信号量sem的值大于等于零为止。 总结:

LINUX实现了POSIX的无名信号量,主要用于线程间的互斥与同步。只要用到以下几个常用函数:

sem_int 用于创建一个信号量,并能初始化它的值。

sem_wait和sem_trywait相当于P操作,它们都能将信号量减一,两者的区别在于信号量小于零时, sem_wait将会阻塞进程,而sem_trywait会立即返回。 sem_post 相当于V操作,它将信号量的值加一同时发出信号唤醒等待线程。 sem_getvalue 用于得到信号量的值。 sem_destroy 用于删除信号量。 所需文件头: 函数原型: 参数说明: #include int sem_init(sem_t *sem , int pashard , unsihned int value) Sem: 信号量 Pashard: 决定信号量能否在几个进程间共享,线程都是在一个进程内,所以这个值只能取0。 Value:信号的初始化值 函数返回值:

其他

所需文件头: #include 成功:0 出错:-1 信号量实际上就是操作系统中所用到的PV原语,它广泛的应用于进程或线

程间的同步与互斥。

int sem_wait (sem_t *sem) int sem_trywait (sem_t *sem) int sem_post (sem_t *sem) int sem_getvalue (sem_t *sem) int sem_destroy (sem:_t *sem) 函数传入值: 函数返回值: Sem: 信好量 成功: 0 出错:-1 例9-5使用信号量实现线程的同步。 操作步骤:

步骤1:编译源代码: [root@localhost ~]vim 9-5.c 程序源代码如下: #include #include #include #include #include #include #include

int lock_var;

time_t end_time; sem_t sem1,sem2;

void *pthread1(void *arg); void *pthread2(void *arg);

int main(){ pthread_t id1,id2;

pthread_t mon_th_id; int ret; end_time = time(NULL)+30;

/*初始化两个信号量一个信号量为1,一个信号量为0*/

ret = sem_init(&sem1,0,1); ret = sem_init(&sem2,0,0); if(ret!=0){ perror(\"sem_init\"); }

/*创建两个线程*/ ret = pthread_create(&id1,NULL,pthread1,NULL);

if(ret !=0) perror(\"线程创建失败!\\n\");

ret = pthread_create(&id2,NULL,pthread2,NULL);

}

if(ret!=0)

perror(\"线程创建失败!\\n\");

pthread_join(id1,NULL); pthread_join(id2,NULL); exit(0);

void *pthread1(void *arg) { int i ; } }

while(time(NULL)sem_wait(&sem2); for(i=0;i<2;i++) { }

sleep(1); lock_var++;

printf(\"lock_var=%d\\n\

printf(\"pthread1:look_var=%d\\n\sem_post(&sem1); sleep(1);

void *pthread2(void *arg){

int nolock=0;

while(time(NULL)printf(\"pthread2:线程1被阻塞,lock_var=%d\\n\sem_post(&sem2); sleep(3); }

步骤2:编译并执行程序,部分结果如下:

附录A 常见的系统调用

本文列出了大部分常见的Linux系统调用,并附有简要中文说明。 以下是Linux系统调用的一个列表,包含了大部分常用系统调用和由系统调用派生出的的函数。这可能是你在互联网上所能看到的唯一一篇中文注释的Linux系统调用列表,即使是简单的字母序英文列表,能做到这么完全也是很罕见的。 按照惯例,这个列表以man pages第2节,即系统调用节为蓝本。按照笔者的理解,对其作了大致的分类,同时也作了一些小小的修改,删去了几个仅供内核使用,不允许用户调用的系统调用,对个别本人稍觉不妥的地方作了一些小的修改,并对所有列出的系统调用附上简要注释。

其中有一些函数的作用完全相同,只是参数不同。(可能很多熟悉C++朋友马上就能联想起函数重载,但是别忘了Linux核心是用C语言写的,所以只能取成不同的函数名)。还有一些函数已经过时,被新的更好的函数所代替了(gcc在链接这些函数时会发出警告),但因为兼容的原因还保留着,这些函数我会在前面标上“*”号以示区别。

一、进程控制:

fork clone execve exit _exit getdtablesize getpgid setpgid getpgrp setpgrp getpid getppid getpriority 创建一个新进程 按指定条件创建子进程 运行可执行文件 中止进程 立即中止当前进程 进程所能打开的最大文件数 获取指定进程组标识号 设置指定进程组标志号 获取当前进程组标识号 设置当前进程组标志号 获取进程标识号 获取父进程标识号 获取调度优先级 setpriority modify_ldt nanosleep nice pause personality prctl ptrace 设置调度优先级 读写进程的本地描述表 使进程睡眠指定的时间 改变分时进程的优先级 挂起进程,等待信号 设置进程运行域 对进程进行特定操作 进程跟踪 sched_get_priority_max 取得静态优先级的上限 sched_get_priority_min 取得静态优先级的下限 sched_getparam sched_getscheduler sched_rr_get_interval sched_setparam sched_setscheduler sched_yield vfork wait wait3 waitpid wait4 capget capset getsid setsid

取得进程的调度参数 取得指定进程的调度策略 取得按RR算法调度的实时进程的时间片长度 设置进程的调度参数 设置指定进程的调度策略和参数 进程主动让出处理器,并将自己等候调度队列队尾 创建一个子进程,以供执行新程序,常与execve等同时使用 等待子进程终止 参见wait 等待指定子进程终止 参见waitpid 获取进程权限 设置进程权限 获取会晤标识号 设置会晤标识号

二、文件系统控制

1、文件读写操作 fcntl open creat close read write readv writev pread pwrite lseek _llseek dup dup2 flock poll 文件控制 打开文件 创建新文件 关闭文件描述字 读文件 写文件 从文件读入数据到缓冲数组中 将缓冲数组里的数据写入文件 对文件随机读 对文件随机写 移动文件指针 在64位地址空间里移动文件指针 复制已打开的文件描述字 按指定条件复制文件描述字 文件加/解锁 I/O多路转换 truncate 截断文件 ftruncate 参见truncate umask fsync 设置文件权限掩码 把文件在内存中的部分写回磁盘 2、文件系统操作 access chdir fchdir 确定文件的可存取性 改变当前工作目录 参见chdir chmod fchmod chown fchown lchown chroot stat lstat fstat statfs fstatfs readdir 改变文件方式 参见chmod 改变文件的属主或用户组 参见chown 参见chown 改变根目录 取文件状态信息 参见stat 参见stat 取文件系统信息 参见statfs 读取目录项 getdents 读取目录项 mkdir mknod rmdir rename link symlink unlink 创建目录 创建索引节点 删除目录 文件改名 创建链接 创建符号链接 删除链接 readlink 读符号链接的值 mount umount ustat utime utimes 安装文件系统 卸下文件系统 取文件系统信息 改变文件的访问修改时间 参见utime quotactl 控制磁盘配额

三、系统控制

ioctl _sysctl acct getrlimit setrlimit getrusage uselib ioperm iopl outb reboot swapon swapoff bdflush sysfs sysinfo adjtimex alarm getitimer setitimer gettimeofday settimeofday stime time times I/O总控制函数 读/写系统参数 启用或禁止进程记账 获取系统资源上限 设置系统资源上限 获取系统资源使用情况 选择要使用的二进制函数库 设置端口I/O权限 改变进程I/O权限级别 低级端口操作 重新启动 打开交换文件和设备 关闭交换文件和设备 控制bdflush守护进程 取核心支持的文件系统类型 取得系统信息 调整系统时钟 设置进程的闹钟 获取计时器值 设置计时器值 取时间和时区 设置时间和时区 设置系统日期和时间 取得系统时间 取进程运行时间 uname vhangup nfsservctl vm86 create_module delete_module init_module query_module 获取当前UNIX系统的名称、版本和主机等信息 挂起当前终端 对NFS守护进程进行控制 进入模拟8086模式 创建可装载的模块项 删除可装载的模块项 初始化模块 查询模块信息 *get_kernel_syms 取得核心符号,已被query_module代替

四、内存管理

brk sbrk mlock munlock mlockall munlockall mmap munmap mremap msync mprotect 改变数据段空间的分配 参见brk 内存页面加锁 内存页面解锁 调用进程所有内存页面加锁 调用进程所有内存页面解锁 映射虚拟内存页 去除内存页映射 重新映射虚拟内存地址 将映射内存中的数据写回磁盘 设置内存映像保护 getpagesize 获取页面大小 sync cacheflush 将内存缓冲区数据写回硬盘 将指定缓冲区中的内容写回磁盘 五、网络管理

getdomainname 取域名 setdomainname 设置域名 gethostid sethostid gethostname sethostname 获取主机标识号 设置主机标识号 获取本主机名称 设置主机名称 六、socket控制

socketcall socket bind connect accept send sendto sendmsg recv recvfrom recvmsg listen select shutdown socket系统调用 建立socket 绑定socket到端口 连接远程主机 响应socket连接请求 通过socket发送信息 发送UDP信息 参见send 通过socket接收信息 接收UDP信息 参见recv 监听socket端口 对多路同步I/O进行轮询 关闭socket上的连接 getsockname 取得本地socket名字 getpeername 获取通信对方的socket名字 getsockopt setsockopt sendfile 取端口设置 设置端口参数 在文件或端口间传输数据 socketpair 创建一对已联接的无名socket 七、用户管理

getuid setuid getgid setgid getegid setegid geteuid seteuid setregid setreuid 获取用户标识号 设置用户标志号 获取组标识号 设置组标志号 获取有效组标识号 设置有效组标识号 获取有效用户标识号 设置有效用户标识号 分别设置真实和有效的的组标识号 分别设置真实和有效的用户标识号 getresgid 分别获取真实的,有效的和保存过的组标识号 setresgid 分别设置真实的,有效的和保存过的组标识号 getresuid 分别获取真实的,有效的和保存过的用户标识号 setresuid 分别设置真实的,有效的和保存过的用户标识号 setfsgid setfsuid 设置文件系统检查时使用的组标识号 设置文件系统检查时使用的用户标识号 getgroups 获取后补组标志清单 setgroups 设置后补组标志清单 八、进程间通信

ipc 进程间通信总控制调用 1、信号 sigaction 设置对指定信号的处理方法 sigprocmask 根据参数对信号集中的信号执行阻塞/解除阻塞等操作 sigpending sigsuspend signal kill *sigblock 为指定的被阻塞信号设置队列 挂起进程等待特定信号 参见signal 向进程或进程组发信号 向被阻塞信号掩码中添加信号,已被sigprocmask代替 *siggetmask 取得现有阻塞信号掩码,已被sigprocmask代替 *sigsetmask 用给定信号掩码替换现有阻塞信号掩码,已被sigprocmask代替 *sigmask *sigpause sigvec ssetmask

将给定的信号转化为掩码,已被sigprocmask代替 作用同sigsuspend,已被sigsuspend代替 为兼容BSD而设的信号处理函数,作用类似sigaction ANSI C的信号处理函数,作用类似sigaction 2、消息 msgctl 消息控制操作 msgget 获取消息队列 msgsnd 发消息 msgrcv 取消息

3、管道 pipe 创建管道

4、信号量 semctl 信号量控制 semget 获取一组信号量 semop 信号量操作

5、共享内存 shmctl 控制共享内存 shmget 获取共享内存 shmat shmdt 连接共享内存 拆卸共享内存

附录B 系统的结构体

task_struct 描述系统中的一个任务或进程

参见 include/linux/sched.h

//进程描述符task_struct struct task_struct {

/* * offsets of these are hardcoded elsewhere - touch with care

*/ volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ //-1 不能运行 0 运行 >0 停止

unsigned long flags; /* per process flags, defined below *///进程标志,在下面定义

int sigpending; //进程上是否有待处理的信号

mm_segment_t addr_limit; /* thread address space:进程地址空间 0-0xBFFFFFFF for user-thead 0-0xFFFFFFFF for kernel-thread */

volatile long need_resched; //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度

int lock_depth; /* Lock depth *///锁深度

/* * offset 32 begins here on 32-bit platforms. We keep * all fields in a single cacheline that are needed for * the goodness() loop in schedule(). */ long counter; //进程可运行的时间量 long nice; //进程的基本时间片

unsigned long policy; //进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR;分时进程:SCHED_OTHER;

struct mm_struct *mm; //进程内存管理信息 int processor;

/* * cpus_runnable is ~0 if the process is not running on any * CPU. It's (1 << cpu) if it's running on a CPU. This mask

* is updated under the runqueue lock.

* * To determine whether a process might run on a CPU, this * mask is AND-ed with cpus_allowed.

* 若进程不在任何CPU上运行,cpus_runnable 的值是0,否则是1。这个值在运行 *队列被锁时更新;*/

unsigned long cpus_runnable, cpus_allowed;

/* * (only the 'next' pointer fits into the cacheline, but * that's just fine.) */

struct list_head run_list; //指向运行队列的指针 unsigned long sleep_time; //进程的睡眠时间

struct task_struct *next_task, *prev_task; //用于将系统中所有的进程连成一个双向循环链表,其根是init_task. struct mm_struct *active_mm;

struct list_head local_pages; //指向本地页面 unsigned int allocation_order, nr_local_pages; /* task state */

struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式 int exit_code, exit_signal;

int pdeath_signal; /* The signal sent when the parent dies *///父进程终止是向子进程发送的信号 /* ??? */

unsigned long personality; //Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序

int did_exec:1; //按POSIX要求设计的布尔量,区分进程正在执行从父进程中继承的代码,还是执行由execve装入的新程序代码 pid_t pid; //进程标识符,用来代表一个进程 pid_t pgrp; //进程组标识,表示进程所属的进程组 pid_t tty_old_pgrp; //进程控制终端所在的组标识 pid_t session; //进程的会话标识

pid_t tgid;

/* boolean value for session group leader */ int leader; //标志,表示进程是否为会话主管 /*

* pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->p_pptr->pid)

*///指针指向(原始的)父进程,孩子进程,比自己年轻的兄弟进程,比自己年长的兄弟进程

//(p->father能被p->p_pptr->pid代替)

struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; struct list_head thread_group; //线程链表 /* PID hash table linkage. *///进程散列表指针

struct task_struct *pidhash_next; //用于将进程链入HASH表pidhash struct task_struct **pidhash_pprev;

wait_queue_head_t wait_chldexit; /* for wait4() *///wait4()使用 struct completion *vfork_done; /* for vfork() */// vfork() 使用

unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值 //it_real_value,it_real_incr用于REAL定时器,单位为jiffies。系统根据it_real_value //设置定时器的第一个终止时间。在定时器到期时,向进程发送SIGALRM信号,同时根据it_real_incr重置终止时间

//it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。当进程运行时,不管在何种状态下,每个tick都使

//it_prof_value值减一,当减到0时,向进程发送信号SIGPROF,并根据it_prof_incr重置时间

//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种状态下,每个tick都使

//it_virt_value值减一,当减到0时,向进程发送信号SIGVTALRM,根据it_virt_incr重置初值。

//Real定时器根据系统时间实时更新,不管进程是否在运行

//Virtual定时器只在进程运行时,根据进程在用户态消耗的时间更新

//Profile定时器在进程运行时,根据进程消耗的时(不管在用户态还是内核态)更新 unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_value; struct timer_list real_timer;//指向实时定时器的指针 struct tms times; //记录进程消耗的时间, unsigned long start_time;//进程创建的时间

long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; //记录进程在每个CPU上所消耗的用户态时间和核心态时间

/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ //内存缺页和交换信息:

//min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换设备读入的页面数);

//nswap记录进程累计换出的页面数,即写到交换设备上的页面数。

//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。在父进程

//回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中 unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; //表示进程的虚拟地址空间是否允许换出 /* process credentials *////进程认证信息

//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid //euid,egid为有效uid,gid

//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件系统的访问权限时使用他们。 //suid,sgid为备份uid,gid uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid;

int ngroups; //记录进程在多少个用户组中 gid_t groups[NGROUPS]; //记录进程所在的组

kernel_cap_t cap_effective, cap_inheritable, cap_permitted;//进程的权能,分别是有效位集合,继承位集合,允许位集合 int keep_capabilities:1; struct user_struct *user; /* limits */

struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息 unsigned short used_math; //是否使用FPU char comm[16]; //进程正在运行的可执行文件名 /* file system info *///文件系统信息 int link_count, total_link_count;

struct tty_struct *tty; /* NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空*/

unsigned int locks; /* How many file locks are being held */ /* ipc stuff *///进程间通信信息

struct sem_undo *semundo; //进程在信号灯上的所有undo操作

struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作

/* CPU-specific state of this task *///进程的CPU状态,切换时,要保存到停止进程的task_struct中

struct thread_struct thread;

/* filesystem information文件系统信息*/ struct fs_struct *fs;

/* open file information *///打开文件信息 struct files_struct *files;

/* signal handlers *///信号处理函数

spinlock_t sigmask_lock; /* Protects signal and blocked */ struct signal_struct *sig; //信号处理函数,

sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位 truct sigpending pending; //进程上是否有待处理的信号 unsigned long sas_ss_sp;

size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; /* Thread group tracking */ u32 parent_exec_id; u32 self_exec_id;

/* Protection of (de-)allocation: mm, files, fs, tty */ spinlock_t alloc_lock;

/* journalling filesystem info */ void *journal_info; };

mm_struct 数据结构用于描述一个任务或进程的虚拟内存 参见 include/linux/sched.h

struct mm_struct { int count;

pgd_t * pgd; /* 进程页目录的起始地址*/ unsigned long context;

unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack, start_mmap; unsigned long arg_start, arg_end, env_start, env_end; unsigned long rss, total_vm, locked_vm; unsigned long def_flags;

struct vm_area_struct * mmap; /* 指向vma双向链表的指针 */ struct vm_area_struct * mmap_avl; /* 指向vma AVL树的指针 */ struct semaphore mmap_sem; }

start_code、end_code:进程代码段的起始地址和结束地址。 start_data、end_data:进程数据段的起始地址和结束地址。

arg_start、arg_end:调用参数区的起始地址和结束地址。 env_start、env_end:进程环境区的起始地址和结束地址。 rss:进程内容驻留在物理内存的页面总数。

vm_area_struct 数据结构描述一个进程的一个虚拟内存区 虚存段vma由数据结构vm_area_struct(include/linux/mm.h)描述:

struct vm_area_struct {

struct mm_struct * vm_mm; /* VM area parameters */ unsigned long vm_start; unsigned long vm_end; pgprot_t vm_page_prot; unsigned short vm_flags;

/* AVL tree of VM areas per task, sorted by address */ short vm_avl_height;

struct vm_area_struct * vm_avl_left; struct vm_area_struct * vm_avl_right;

/* linked list of VM areas per task, sorted by address */ struct vm_area_struct * vm_next;

/* for areas with inode, the circular list inode->i_mmap */ /* for shm areas, the circular list of attaches */ /* otherwise unused */

struct vm_area_struct * vm_next_share; struct vm_area_struct * vm_prev_share; /* more */

struct vm_operations_struct * vm_ops; unsigned long vm_offset; struct inode * vm_inode;

unsigned long vm_pte; /* shared mem */

};

vm_start;//所对应内存区域的开始地址 vm_end; //所对应内存区域的结束地址 vm_flags; //进程对所对应内存区域的访问权限 vm_avl_height;//avl树的高度 vm_avl_left; //avl树的左儿子 vm_avl_right; //avl树的右儿子

vm_next;// 进程所使用的按地址排序的vm_area链表指针 vm_ops;//一组对内存的操作

这些对内存的操作是当对虚存进行操作的时候Linux系统必须使用的一组方法。比如说,当进程准备访问某一虚存区域但是发现此区域在物理内存不存在时(缺页中断),就激发某种对内存的操作执行正确的行为。这种操作是空页(nopage)操作。当Linux系统按需调度可执行的页面映象进入内存时就使用这种空页(nopage)操作。

当一个可执行的页面映象映射到进程的虚存地址时,一组vm_area_struct结构的数据结构(vma)就会生成。每一个 vm_area_struct的数据结构(vma)代表可执行的页面映象的一部分:可执行代码,初始化数据(变量),非初始化数据等等。Linux系统可以支持大量的标准虚存操作,当vm_area_struct数据结构(vma)一被创建,它就对应于一组正确的虚存操作。 属于同一进程的vma段通过vm_next指针连接,组成链表。如图2-3所示,struct mm_struct结构的成员struct vm_area_struct * mmap 表示进程的vma链表的表头。 为了提高对vma段 查询、插入、删除操作的速度,LINUX同时维护了一个AVL(Adelson-Velskii and Landis)树。在树中,所有的vm_area_struct虚存段均有左指针vm_avl_left指向相邻的低地址虚存段,右指针 vm_avl_right指向相邻的高地址虚存段,如图2-5。struct mm_struct结构的成员struct vm_area_struct * mmap_avl表示进程的AVL树的根,vm_avl_height表示AVL树的高度。

对平衡树mmap_avl的任何操作必须满足平衡树的一些规则: Consistency and balancing rulesJ(一致性和平衡规则):

tree->vm_avl_height==1+max(heightof(tree->vm_avl_left),heightof( tree->vm_avl_right))

abs( heightof(tree->vm_avl_left) - heightof(tree->vm_avl_right) ) <= 1 foreach node in tree->vm_avl_left: node->vm_avl_key <= tree->vm_avl_key, foreach node in tree->vm_avl_right: node->vm_avl_key >= tree->vm_avl_key.

注:其中node->vm_avl_key= node->vm_end 对vma可以进行加锁、加保护、共享和动态扩展等操作。

file

每一个打开的文件、 socket 等等都用一个 file 数据结构代表 参见 include/linux/fs.h

struct file {

mode_t f_mode; loff_t f_pos;

unsigned short f_flags; unsigned short f_count;

unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct file *f_next, *f_prev;

int f_owner; /* pid or -pgrp where SIGIO should be sent */ struct inode * f_inode; struct file_operations * f_op; unsigned long f_version;

void *private_data; /* needed for tty driver, and maybe others */ };

file_struct 数据结构描述了一个进程打开的文件 参见 include/linux/sched.h

struct files_struct { int count;

fd_set close_on_exec; fd_set open_fds;

struct file * fd[NR_OPEN]; };

file_operations 文件操作

struct file_operations { struct module *owner;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t);

unsigned int (*poll) (struct file *, struct poll_table_struct *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int);

int (*lock) (struct file *, int, struct file_lock *);

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); #ifdef MAGIC_ROM_PTR

int (*romptr) (struct file *, struct vm_area_struct *); #endif /* MAGIC_ROM_PTR */ };

fs_struct

参见 include/linux/sched.h

struct fs_struct { int count;

unsigned short umask; struct inode * root, * pwd; } ;

因篇幅问题不能全部显示,请点此查看更多更全内容