目    录

第1章 Linux 快速入门... 3

1.1  嵌入式 Linux 基础... 3

1.2  Linux 安装... 4

1.3  Linux 文件及文件系统... 7

1.4 实验内容——安装 Linux 操作系统... 9

第2章 Linux 基础命令... 10

2.1  Linux 常用操作命令... 10

2.4  实验内容... 29

第3章 Shell程序设计... 31

3.1  Linux 脚本编写基础... 31

3.2  管道和重定向... 34

3.3  shell的语法及控制结构... 35

3.4 正则表达式... 51

3.5 Shell调试技术... 59

第4章 Linux 下的 C 编程基础... 62

4.1  Linux 下 C 语言编程概述... 62

4.2  进入 Vi(vi 文件名). 63

4.3  Gcc 编译器... 66

4.4  Gdb 调试器... 74

4.5  Make 工程管理器... 85

4.6 使用 autotools. 92

4.7  实验内容... 99

第5章 嵌入式 Linux 开发环境的搭建... 108

5.1 嵌入式交叉编译环境的搭建... 108

5.2  超级终端和 Minicom 配置及使用... 110

5.3  下载映像到开发板... 118

5.4  编译嵌入式 Linux 内核... 123

5.5  Linux 内核目录结构... 128

5.6  制作文件系统... 129

第6章 文件 I/O 编程... 133

6.1  Linux 系统调用及用户编程接口(API)... 133

6.2  Linux 中文件及文件描述符概述... 134

6.3  不带缓存的文件 I/O 操作... 135

6.4  嵌入式 Linux 串口应用开发... 153

6.5  标准 I/O 开发... 166

6.6  实验内容... 174

第7章 进程控制开发... 181

7.1  Linux 下进程概述... 181

7.2  Linux 进程控制编程... 185

7.3  Linux 守护进程... 200

7.4  实验内容... 210

第8章 进程间通信... 212

8.1         Linux 下进程间通信概述... 212

8.2  管道通信... 213

8.3  信号通信... 227

8.4  共享内存... 239

8.5  消息队列... 244

8.6  实验内容... 248

第9章 多线程编程... 250

9.1  Linux 下线程概述... 250

9.2  Linux 线程实现... 252

9.3  实验内容——“生产者消费者”实验... 274

第10章 嵌入式 Linux 网络编程... 275

10.1  TCP/IP 协议概述... 275

10.2  网络基础编程... 280

10.3  网络高级编程... 297

10.4  实验内容——NTP 协议实现... 303

第11章 嵌入式Linux设备驱动编程... 305

11.1 设备驱动编程基础... 305

11.2 字符设备驱动编程... 315

11.3 GPIO驱动程序实例... 316

11.4 按键驱动程序实例... 316

第12章 Qt图形编程... 316

12.1 嵌入式GUI简介... 316

12.2 Qt/Embedded开发入门... 317

12.3 实验内容:使用Qt编写“Hello World”程序... 321



第1章 Linux 快速入门

1.1  嵌入式 Linux 基础

1.1.1  Linux 发展概述

      简单地说,Linux 是指一套免费使用和自由传播的类 UNIX 操作系统。

GNU(GNU’s Not UNIX) 是为了推广自由软件的精神以实现一个自由的操作系统,然后从应用程序开始,实现其内核。其中的程序开发共同遵守 General Public License(GPL)协议,这是最开放也是最严格的许可协议方式,这个协议规定了源码必须可以无偿的获取并且修改。因此,从严格意义上说,Linux 应该叫做GNU/Linux,其中许多重要的工具如 gcc、gdb、make、Emacs 等都是 GNU 贡献。

Linux 的内核版本号:(获取命令:uname –a/uname –r/cat /proc/version)

Linux 内核版本号格式是 x.y.zz-www,数字 x 代表版本类型,数字 y 为偶数时是稳定版本,为奇数时是开发版本,如2.0.40为稳定版本,2.3.42 为开发版本,测试版本为 3 个数字加上测试号,如 2.4.12-rc1。最新的 Linux 内核版本可从 http://kernel.org 上获得。

1.1.2  Linux 作为嵌入式操作系统的优势

1.低成本开发系统

Linux  的源码开放性允许任何人可以获取并修改 Linux的源码

2.可应用于多种硬件平台

3.可定制的内核

Linux能根据嵌入式设备的个性需要量体裁衣,经裁减的 Linux内核最小可达到 150KB 以下,尤其适合嵌入式领域中资源受限的实际情况。

4.性能优异

Linux系统内核精简、高效和稳定,能够充分发挥硬件的功能,因此它比其他操作系统的运行效率更高。

5.良好的网络支持

Linux 是首先实现 TCP/IP 协议栈的操作系统。

1.1.3  Linux 发行版本

1.Red Hat

目前 Red Hat 分为的企业版(收费)和免费的桌面版 Fedora Core。

Red Hat 企业版有三个版本——AS、ES 和 WS。AS 是其中功能最为强大和完善的版本。

而正统的桌面版 Red Hat 版本更新早已停止,最后一版是 Red Hat 9.0。

2.Debian

Debian 系统分为三个版本,分别为稳定版(Stable),测试版(Testing)和不稳定版(Unstable)

官方主页:http://www.debian.org/

3.国内的发行版本及其他

目前国内的红旗、新华等都发行了自己的 Linux 版本。

1.2  Linux 安装

1.2.1  基础概念

1.文件系统、分区和挂载

Linux文件系统是一个文件树,且它的所有文件和外部设备(如硬盘、光驱等)都是以文件的形式挂结在这个文件树上,例如“\usr\local”。 Linux 中把每一个分区和某一个目录对应,以后在对这个目录的操作就是对这个分区的操作,这个把分区和目录对应的过程叫做挂载(Mount),而这个挂载在文件树中的位置就是挂载点。

2.主分区、扩展分区和逻辑分区

硬盘分区是针对一个硬盘进行操作的,它可以分为:主分区、扩展分区、逻辑分区。一般而言,对于先装了 Windows 的用户,则 Windows 的 C 盘是装在主分区上的,可以把Linux   安装在另一个主分区或者扩展分区上。通常为了安装方便安全起见,一般采用把Linux 装在多余的逻辑分区上。如图 1.4 所示。

          图    Linux 安装的分区示意图

3.SWAP 交换分区

类似于Windows 的虚拟内存,Linux 把swap叫做交换分区。一般将其设为内存大小的2倍,当然也可以设为更大。

4.分区格式

EXT2/EXT3/XFS/FAT/NTFS。

5.GRUB

GRUB是一种引导装入器(类似在嵌入式中非常重要的bootloader)——它负责装入内核并引导 Linux 系统,位于硬盘的起始部分。

6.root 权限

root 的默认主目录在“/root”下,而其他普通用户的目录则在“/home”下。

1.2.2  安装过程(Fedora Core 6)

本安装过程以Red Hat 9 桌面版为例,内核版本为2.6.18-1.2798.fc6,采用镜像文件系统iso虚拟光驱,过程如下:


一、先安装虚拟软件工作平台Vmware Workstation,步骤如下:

运行VMware. 6.0.2.exe

→自定义安装

→选择安装目录

→注册即输入SN(由vmware.6.0.2解压得到zwt_vmw6.exe,运行该文件,在type下拉列表框中选择vmware workstation 6.0(windows),然后单击generate生成serial,这就是我们要的SN)

→双击vmware workstation 桌面图标

→new virtual machine

→custom

→next step

→linux

→输入名字和目录(可以采用默认)

→one(单一处理器)

→设置虚拟机使用内存512M

→use bridged networking

→LSI logic

→create a new virtual disk

→scsi

→设置可用磁盘大小(20G)

→完成

二、安装linux(采用虚拟光驱),步骤如下:

    先挂载光驱的镜像文件(browse指明)和关闭软驱检测

→start this virtual machine

→在窗体内回车键

→skip

→next

→选择安装过程使用的语言(简体中文)

→选择键盘(默认)

→跳出的提示按“是”

→选择“建立自定义的分区结构”

→交换分区swap大小为虚拟内存的2倍,其余的为根分区 “/” (ext3),并选中“强制为主分区”项

→默认下一步

→默认下一步

→时区选择默认

→为root用户输入密码

→选择“现在定制”需要的软件

→进行软件选择(如果全选,安装慢,而且有的根本不会用到,确保“虚拟化”和“老的软件”不选,否则会影响Vmware tools的安装使用)

→下一步开始安装fedora core(安装时间依赖于选择要安装的软件大小)

→重新启动

→防火墙禁用

→“是”

→SElinux禁用

→“是”

→创建新用户(可以跳过)

→测试声卡

→重新启动完成安装。(约半个小时)

     三、安装VMware tools,以方便鼠标内外移动(确保“虚拟化”不选),步骤如下:

VM菜单

→VMware tools install

→“是”

→将光驱(linux中的光驱)的VMware tools 中的Vmwaretools-6.0.2-59824.tar.gz复制到目录root下

→在终端对Vmwaretools-6.0.2-59824.tar.gz进行解压tar –zxvf  Vmwaretools-6.0.2-59824.tar.gz

→cd vmware-tools-distrib

→./vmware-install.pl(全部选yes,注意有一个默认为[no]的选项改为yes)

四、设置windows与linux相互共享:

Windows下访问linux的内容:

确保网线正确连接;

确保网络端口激活(系统→管理→网络→设置静态IP并激活);

确保samba服务安装并启动(系统→管理→服务器设置→services中勾选smb选项);

添加samba用户(必须是存在的用户,如果冲突,则注释/etc/samba/smbusers中对应的用户名)(“首选项”→samba用户→添加用户);

添加共享(选择共享目录和设置共享对象权限等)。

       通过\\192.168.11.7即“\\虚拟机ip”的形式访问。


五、Linux下访问windows的内容:

确保安装时没有选择“虚拟化”选项;

菜单VM

→settings

→options

→shared folds

→add。

在linux系统下通过/mnt/hgfs目录访问windows中共享文件夹。


1.3  Linux 文件及文件系统

1.3.1  文件类型及文件属性

1.文件类型

Linux中主要的文件类型分为 4 种:普通文件、目录文件、链接文件和设备文件。

(1)普通文件(-)

(2)目录文件(d)

(3)链接文件(l)

(4)设备文件

设备文件一般都在/dev 目录下,包括块设备文件(b)字符设备文件(c)。块设备文件是指数据的读写,它们是以块(如由柱面和扇区编址的块)为单位的设备,最简单的如硬盘(/dev/hda1)等。字符设备主要是指串行端口的接口设备。

2.文件属性

Linux 中文件的拥有者可以把文件的访问属性设成 3 种不同的访问权限:可读(r)、可写(w)和可执行(x)。文件又有 3 个不同的用户级别:文件拥有者(u)、所属的用户组(g)和系统里的其他用户(o)。

第一个字符显示文件的类型:

 “-”表示普通文件;

 “d”表示目录文件;

 “l”表示链接文件;

 “c”表示字符设备;

 “b”表示块设备;

 “p”表示命名管道比如 FIFO 文件(First In First Out,先进先出);

 “f”表示堆栈文件比如 LIFO 文件(Last In First Out,后进先出)。

第一个字符之后有 3 个三位字符组:

 第一个三位字符组表示对于文件拥有者(u)对该文件的权限;

 第二个三位字符组表示文件用户组(g)对该文件的权限;

 第三个三位字符组表示系统其他用户(o)对该文件的权限;

若该用户组对此没有权限,一般显示“-”字符。

目录权限和文件权限有一定的区别。对于目录而言,r 代表允许列出该目录下的文件和子目录,w 代表允许生成和删除该目录下的文件,x 代表允许访问该目录。

1.3.2  文件系统类型介绍

1.ext2 和

2.swap 文件系统

在安装 Linux 的时候,交换分区是必须建立的,并且它所采用的文件系统类型必须是 swap,而没有其他选择。

3.vfat 文件系统

4.NFS 文件系统

NFS 文件系统是指网络文件系统,这种文件系统也是 Linux 的独到之处。它可以很方便地在局域网内实现文件共享,并且使多台主机共享同一主机上的文件系统。

5.ISO9660 文件系统

这是光盘所使用的文件系统,在 Linux 中对光盘已有了很好的支持,它不仅可以提供对光盘的读写,还可以实现对光盘的刻录。

1.3.3 Linux 目录结构

Linux文件系统采用带链接的树形结构,即只有一个根目录(通常用“/”表示),如下图所示。


/bin :二进制(binary)英文缩写。

/boot :存放系统启动时要用到的程序,在使用grub或lilo引导linux的时候,会用到这里的一些信息。

/dev:目录中包含了所有linux系统中使用的外部设备,但是这里放的并不是外部设备的驱动程序。

/etc :是linux系统中最重要的目录之一,存放了系统管理时要用到的各种配置文件和子目录。

/sbin :存放系统管理员的系统管理程序。

/home :存放用户的主目录。

/lib :存放系统动态连接共享库的,几乎所有的应用程序都会用到这个目录下的共享库。

/mnt :这个目录在一般情况下也是空的,可以临时将别的文件系统挂在这个目录下。

/proc :可以在这个目录下获取系统信息,这些信息是在内存中,由系统自己产生的。

/root :超级用户的主目录。

/tmp :用来存放不同程序执行时产生的临时文件。

/usr :这是linux系统中占用硬盘空间最大的目录。

/sys:  这是 Linux2.6 内核的一个很大的变化。该目录下安装了 2.6 内核中新出现的一个文件系统 sysfs,sysfs 文件系统集成了下面 3 种文件系统的信息:针对进程信息的 proc 文件系统、针对设备的 devfs 文件系统以及针对伪终端的 devpts 文件系统。

/var :这也是一个非常重要的目录,很多服务的日志信息都存放在这里。

1.4 实验内容——安装 Linux 操作系统

思考与练习:若有一个文件,其属性为“-rwxr—rw-”,说出这代表的什么?

第2章 Linux 基础命令

2.1  Linux 常用操作命令

启动Linux之后,进入的这个界面就是 Linux 图形化界面 X 窗口系统的一部分。要注意的是,X 窗口系统仅仅是 Linux上面的一个软件,不是 Linux 自身的一部分。建议读者尽可能地使用 Linux 的命令行界面,也就是 Shell 环境。

Shell与用户间的关系如图 2.1 所示。用户在提示符下输入的命令都由Shell 先解释然后传给 Linux 内核。

图    内核、Shell 和用户的关系

小知识:命令的使用可以通过man 命令名或命令名 -–help的方式获得帮助。

2.1.1  用户系统相关命令

Linux是一个多用户的操作系统,常见的环境变量如下。

PATH     是系统路径。

HOME       是系统根目录。

HISTSIZE   是指保存历史命令记录的条数。

LOGNAME    是指当前用户的登录名。

☆SHELL       是指当前用户用的是哪种 Shell。

☆LANG/LANGUGE 是和语言相关的环境变量,使用多种语言的用户可以修改此环境变量。

☆MAIL        是指当前用户的邮件存放目录。

设置环境变量方法如下:

通过 echo 显示字符串(指定环境变量)。

 通过 export 设置新的环境变量。

 通过 env 显示所有环境变量。

 通过 set 命令显示所有本地定义的 Shell 变量。

 通过 unset 命令来清除环境变量。

1、用户切换(su)

2、用户管理(useradd 和 passwd)

3、系统管理命令(ps 和 kill)

   ps:显示当前系统中由该用户运行的进程列表。

kill:输出特定的信号给指定PID(进程号)的进程,并根据该信号而完成指定的行为。

4、磁盘相关命令(fdisk)

     (1)作用

        fdisk 可以查看硬盘分区情况,并可对硬盘进行分区管理。如

[root@ateng ~]#fdisk -l

(2)使用说明

使用 fdisk 必须拥有 root 权限

5、 磁盘挂载命令(mount)

(1)作用

mount 命令就可以把文件系统挂载到相应的目录下,并且由于 Linux 中把设备都当作文件一样使用,因此,mount 命令也可以挂载不同的设备。通常,在  Linux 下“/mnt”目录是专门用于挂载不同的文件系统的,它可以在该目录下新建不同的子目录来挂载不同的设备文件系统。

(2)格式

mount [选项]  [类型] 设备文件名 挂载点目录

其中的类型是指设备文件的类型。

(3)常见参数

mount 常见参数如表 2.8 所示。


(4)使用实例

 [root@ateng mnt]#mount -t vfat /dev/sdb1 /mnt/usb

[root@ateng mnt]# cd /mnt/usb

24.s03e01.pdtv.xvid-sfm.rmvb  Documents and Settings  Program Files

24.s03e02.pdtv.xvid-sfm.rmvb  Downloads                     Recycled

可见,在挂载了 C 盘之后,可直接访问 Windows下的 C 盘的内容。

⑤  在使用完该设备文件后可使用命令 umount 将其卸载。

[root@ateng mnt]#   umount /mnt/usb

[root@ateng mnt]# cd /mnt/c

[root@ateng c]# ls

另:挂载Windows目录如

mount -o username=administrator,password=pldy123,iocharset=utf8 //172.21.28.71/c /mnt/c 

若想设置在开机时自动挂载,可在文件“/etc/fstab”中加入相应的设置行即可。

2.1.2  文件目录相关命令

1.cd

(1)作用

改变工作目录。

(2)格式

cd [路径]

其中的路径为要改变的工作目录,可为相对路径或绝对路径。

(3)使用实例

[root@www uclinux]# cd /home/ateng/

[root@localhost ~]# pwd

[root@localhost ~]# /home/ateng/

(4)使用说明

可使用“cd –”可以回到前次工作目录。“./”代表当前目录,“../”代表上级目录

2.ls

(1)作用

列出目录的内容。

(2)格式:ls [选项] [文件]

(3)常见参数

(4)使用实例

[yuling@www /]$ ls -l

total 220

drwxr-xr-x    2 root     root         4096 Mar 31  2005 bin

drwxr-xr-x    3 root     root         4096 Apr  3  2005 boot

     显示格式说明如下。

文件类型与权限  链接数  文件属主   文件属组   文件大小   修改的时间 名字

(5)使用说明

在 ls 的常见参数中,-l(长文件名显示格式)的选项是最为常见的。可以详细显示出各种信息。

若想显示出所有“.”开头的文件,可以使用-a,这在嵌入式的开发中很常用。

3.mkdir(可以一次建立多个目录)

(1)作用

创建一个目录。

(2)格式

mkdir [选项]  路径

(3)常见参数

mkdir 主要选项参数如表 2.10 所示

(4)使用实例

[root@localhost ~]# mkdir -p ./hello/my

该实例使用选项“-p”一次创建了./hello/my 多级目录。

[root@localhost ~]#

[root@www my]# ls -l

total 4   drwxrwxrwx    2 root     root         4096 Jan 14 09:24 why

该实例使用选项“-m”创建了相应权限的目录。

(5)使用说明

该命令要求创建目录的用户在创建路径的上级目录中具有写权限,并且路径名不能是当前目录中已有的目录或文件名称。

4.cat

连接并显示指定的一个和多个文件的有关信息。如查看系统服务及其对应端口号命令为:cat /etc/services

5.cp、mv 和 rm

(1)作用

①  cp:将给出的文件或目录复制到另一文件或目录中。

②  mv:为文件或目录改名或将文件由一个目录移入另一个目录中。

③  rm:删除一个目录中的一个或多个文件或目录。

(2)格式

①  cp:cp [选项]     源文件或目录    目标文件或目录。

②  mv:mv [选项]       源文件或目录   目标文件或目录。

③  rm:rm [选项]      文件或目录。

(3)常见参数

①  cp 主要选项参数见表 2.12 所示。

②  mv 主要选项参数如表 2.13 所示

③  rm 主要选项参数如表 2.14 所示。

(3)使用说明

①  cp:该命令把指定的源文件复制到目标文件或把多个源文件复制到目标目录中。

②  mv:

  该命令根据命令中第二个参数类型的不同(是目标文件还是目标目录)来判断是重命名还是移动文件,当第二个参数类型是文件时,mv命令完成文件重命名,此时,它将所给的源文件或目录重命名为给定的目标文件名;

当第二个参数是已存在的目录名称时,mv 命令将各参数指定的源文件均移至目标目录中;

在跨文件系统移动文件时,mv 先复制,再将原有文件删除,而链至该文件的链接也将丢失。

③  rm:

如果没有使用- r 选项,则 rm 不会删除目录;

  使用该命令时一旦文件被删除,它是不能被恢复的,所以最好使用-i参数

6.chown 和

(1)作用

①  chown:修改文件所有者和组别。

②  chgrp:改变文件的组所有权。

7.chmod

(1)作用

改变文件的访问权限。

(2)格式

chmod的格式有两种不同的形式。

①  符号标记:chmod [选项]…符号权限[符号权限]…文件

多个用户级别的权限中间要用逗号分开表示,若没有显示指出则表示不作更改。

②  八进制数:chmod [选项]   …八进制权限  文件…

其中的八进制权限是指要更改后的文件权限。

(3)选项参数

(4)使用实例

文件的访问权限可表示成:-  rwx  rwx  rwx。在此设有三种不同的访问权限:读(r)、写(w)和运行(x)。三个不同的用户级别:文件拥有者(u)、所属的用户组(g)和系统里的其他用户(o)。在此,可增加一个用户级别 a(all)来表示所有这三个不同的用户级别。

①  对于第一种符号连接方式的 chmod 命令中,用加号“+”代表增加权限,用减号“−”删除权限,等于号“=”设置权限

例如原先文件 uClinux20031103.tgz,其权限如下所示。

[root@localhost ~]# ls   –l

-rw-r--r--    1 root     root     79708616 Mar 24  2005 uClinux20031103.tgz

[root@localhost ~]# chmod a+rx,u+w uClinux20031103.tgz

[root@localhost ~]# ls   –l

-rwxr-xr-x    1 root     root     79708616 Mar 24  2005 uClinux20031103.tgz

可见,在执行了 chmod 之后,文件拥有者除拥有所有用户都有的可读和执行的权限外,还有可写的权限。

②  对于第二种八进制数指定的方式,将文件权限字符代表的有效位设为“1”,即“rw-”、“rw-”和“r--”的八进制表示为“110”、“110”、“100”,把这个 2 进制串转换成对应的 8 进制数就是 6、6、4,也就是说该文件的权限为 664(三位八进制数)。

文件0.5.1.tar.gz,其权限如下所示。

[root@localhost ~]# ls   –l

-rw-rw-r--    1  ateng     ateng        20543 Dec 29  2004 genromfs-0.5.1.tar.gz

[root@localhost ~]# chmod 765 genromfs-0.5.1.tar.gz

[root@localhost ~]# ls –l

-rwxrw-r-x    1 ateng     ateng        20543 Dec 29  2004 genromfs-0.5.1.tar.gz

可见,在执行了 chmod 765 之后,该文件的拥有者权限、文件组权限和其他用户权限都恰当地对应了。

(5)使用说明

  使用 chmod 必须具有 root 权限

想一想,chmod o+x uClinux20031103.tgz 是什么意思?它所对应的 8 进制数指定更改应如何表示?

8.grep

(1)作用

在指定文件中搜索特定的内容,并将含有这些内容的行标准输出。

9.find

(1)作用

在指定目录中搜索文件,它的使用权限是所有用户。

(2)格式

find [路径][选项][描述]

其中的路径为文件搜索路径,系统开始沿着此目录树向下查找文件。它是一个路径列表,相互用空格分离。若缺省路径,那么默认为当前目录。

其中的描述是匹配表达式,是 find 命令接受的表达式。

(3)常见参数

[选项]主要参数如表 2.19 所示。

(4)使用实例

 [root@localhost ~]# find ./ -name qiong*.c

./qiong1.c

./iscit2005/qiong.c

在该实例中使用了-name 的选项支持通配符。

(5)使用说明

若使用目录路径为“/”,通常需要查找较多的时间,可以指定更为确切的路径以减少查找时间。

find 命令可以使用混合查找的方法,例如,想在/etc 目录中查找大于 500000 字节,并且在 24 小时内修改的某个文件,则可以使用-and(与)把两个查找参数链接起来组合成一个混合的查找方式,如“find /etc -size +500000c -and -mtime +1”

10.locate

用于查找文件。

11.ln

(1)作用

为某一个文件在另外一个位置建立一个符号链接。

(2)格式

ln[选项]     源文件   链接文件

(3)常见参数

  s 建立符号链接(这也是通常惟一使用的参数)

(4)使用实例

[root@www uclinux]# ln -s ../genromfs-0.5.1.tar.gz ./hello

[root@www uclinux]# ls -l

total 77948

lrwxrwxrwx   1 root   root    24 Jan 14 00:25 hello -> ../genromfs-0.5.1.tar.gz

该实例建立了当前目录的 hello 文件与上级目录之间的符号连接,可以看见,在 hello 的ls –l 中的第一位为“l”,表示符号链接,同时还显示了链接的源文件

(5)使用说明

  ln 命令会保持每一处链接文件的同步性,也就是说,不论改动了哪一处,其他的文件都会发生相同的变化。

2.1.3  压缩打包相关命令

  1.gzip

(1)作用

  对单个文件进行压缩和解压缩,而且 gzip 根据文件类型可自动识别压缩或解压。

(2)格式

gzip [选项] 压缩(解压缩)的文件名。

(3)常见参数

gzip 主要选项参数如表 2.23 所示。

(4)使用实例

[root@www my]# gzip hello.c

[root@www my]# ls

hello.c.gz

[root@www my]# gzip -l hello.c

   compressed              uncompressed  ratio uncompressed_name

61                       39.3% hello.c

该实例将目录下的“hello.c”文件进行压缩,选项“-l”列出了压缩比。

(5)使用说明

使用 gzip压缩只能压缩单个文件,而不能压缩目录,其选项“-d”是将该目录下的所有文件逐个进行压缩,而不是压缩成一个文件。

2.tar

(1)作用

对文件目录进行打包或解包

打包是指将一些文件或目录变成一个总的文件,而压缩则是将一个大的文件通过一些压缩算法变成一个小文件。

(2)格式

tar [选项] [打包后文件名]  文件目录列表。

tar 可自动根据文件名识别打包或解包动作,其中打包后文件名为用户自定义的打包后文件名称,文件目录列表可以是要进行打包备份的文件目录列表,也可以是进行解包的文件目录列表。

(3)主要参数

tar 主要选项参数如表 2.24 所示。

(4)使用实例

[root@www home]# tar -cvf

./yul/

……………………

./yul/my/why.c.gz

[root@www home]# ls -l yul.tar

-rw-r--r--    1 root     root        10240 Jan 14 15:01 yul.tar

该实例将“./yul”目录下的文件加以打包,其中选项“-v”在屏幕上输出了打包的具体过程。

[root@localhost ~]# tar -zxvf linux-2.6.11.tar.gz

linux-2.6.11/

linux-2.6.11/drivers/

linux-2.6.11/drivers/video/

linux-2.6.11/drivers/video/aty/

该实例用选项“-z”调用 gzip,并-x 联用时完成解压缩

(5)使用说明

tar 命令除了用于常规的打包之外,使用更为频繁的是用选项“-z”或“-j”调用 gzip 或

bzip2(Linux 中另一种解压工具)完成对各种不同文件的解压

表 2.25 对 Linux 中常见类型的文件解压命令做一总结。

2.1.4  比较合并文件相关命令

diff和patch

2.1.5  网络相关命令

1.ifconfig

(1)作用

用于查看和配置网络接口的地址和参数,包括 IP 地址、网络掩码、广播地址,它的使用权限是超级用户。

(2)格式

ifconfig 有两种使用格式,分别用于查看和更改网络接口。

①  ifconfig [选项] [网络接口]:用来查看当前系统的网络配置情况。

②  ifconfig  网络接口  [选项]  地址:用来配置指定接口(如 eth0,eth1)的 IP 地址、网络掩码、广播地址等。

(3)常见参数

ifconfig 第二种格式常见选项参数如表 2.29 所示。

   (4)使用实例

首先,在本例中使用 ifconfig 的第一种格式来查看网口配置情况。

[root@ateng workplace]#ifconfig

结果中详细列出了所有活跃接口的 IP 地址、硬件地址、广播地址、子网掩码、回环地址等。

[root@ateng workplace]#ifconfig eth0

此例中,通过指定接口显示出对应接口的详细信息。另外,用户还可以通过指定参数“-a”来查看所有接口(包括非活跃接口)的信息。

 [root@ateng ~]# ifconfig eth0 down

[root@ateng ~]# ifconfig

在此例中,通过将指定接口的状态设置为 DOWN,暂时暂停该接口的工作。

[root@ateng workplace]# ifconfig eth0 210.25.132.142 netmask 255.255.255.0

可以看出,ifconfig 改变了接口 eth0 的 IP 地址、子网掩码等,在之后的 ifconfig查看中可以看出确实发生了变化。

(5)使用说明

用 ifconfig 命令配置的网络设备参数不需重启就可生效,但在机器重新启动以后将会失效

2.ftp

(1)作用

该命令允许用户利用 ftp 协议上传和下载文件。

2.2  Linux 启动过程详解

2.2.1  概述

开机时CPU 将自动进入实模式,BIOS进行开机自检,并按 BIOS(嵌入式设备对应的是NOR FLASH)中设置的启动设备(通常是硬盘)进行启动,接着启动设备上安装的引导程序lilo或grub(嵌入式设备的bootloader)开始引导 Linux

第二阶段,内核的引导。主要完成磁盘引导、读取机器系统数据、实模式和保护模式的切换、加载数据段寄存器以及重置中断描述符表等。

第三阶段执行 init 程序(也就是系统初始化工作),init 程序调用了 rc.sysinit rc 等程序,而 rc.sysinit 和 rc 在完成系统初始化和运行服务的任务后,返回 init。

第四阶段,init 启动 mingetty,打开终端供用户登录系统,用户登录成功后进入了Shell,这样就完成了从开机到登录的整个启动过程。

Linux 启动总体流程图如图 2.2 所示。

  

 图    Linux 启动总体流程图


2.2.2  内核引导阶段

   在 grub 或 lilo 等引导程序成功完成引导 Linux 系统的任务后,Linux 就从它们手中接管了 CPU 的控制权。用户可以从 kernel.org 上下载最新版本的源码进行阅读,其目录为:

linux-2.6.*.*\arch\i386\boot。在这过程中主要用到该目录下的这几个文件:bootsect.S、setup.S以及 compressed 目录下的 head.S 等。

(1)bootsect 阶段

完成指令搬移,执行权转到setup.S 的程序中。

(2)setup 阶段

读取机器系统数据,保存系统参数到内存,检测和设置显示器和显示模式,进入 32 位保护模式跳转并执行“arch/i386/kernel/head.S”中的 startup_32。

(3)head.S 阶段

内核解压,完成寄存器、分页表的初始化工作,跳转到 start_kernel()函数。

(4)main.c 阶段

start_kernel()函数进行内核的初始化,start_kernel()的最后,调用了 init()函数。

2.2.3  init 阶段

内核执行引导的第一个进程就是 INIT 进程,该进程号始终是“1”。INIT进程根据其配置文件“/etc/inittab”主要完成系统的一系列初始化的任务。

inittab 文件中除了注释行外,每一行都有如下格式:

id:runlevels:action:process

(1)id

id 是配置记录标识符,由 1~4 个字符组成,对于 getty 或 mingetty 等其他 login 程序项,要求 id 与 tty 的编号相同,否则 getty 程序将不能正常工作。

(2)runlevels

runlevels 是运行级别记录符,一般使用 0~6 以及 S 和 s。其中,0、1、6 运行级别为系统保留:0 作为 shutdown 动作,1 作为重启至单用户模式,6 为重启;S 和 s 意义相同,表示单用户模式,且无需 inittab 文件,因此也不在 inittab 中出现。runlevel 可以是并列的多个值,对大多数 action 来说,仅当 runlevel与当前运行级别匹配成功才会执行。

(3)action

action 字段用于描述系统执行的特定操作,它的常见设置有:initdefault、sysinit、boot、bootwait、respawn 等。

initdefault 用于标识系统缺省的启动级别。当 init 由内核激活以后,它将读取 inittab 中的initdefault 项,取得其中的 runlevel,并作为当前的运行级别。如果没有 inittab 文件,或者其中没有 initdefault 项,init 将在控制台上请求输入 runlevel。

sysinit、boot、bootwait 等 action 将在系统启动时无条件运行,忽略其中的 runlevel。

respawn 字段表示该类进程在结束后会重新启动运行。

(4)process

process 字段设置启动进程所执行的命令

inittab 配置文件完成的功能:

1.确定用户登录模式

在“/etc/inittab”中列出了如下所示的登录模式,主要有单人维护模式、多用户无网络模式、文字界面多用户模式、X-Windows 多用户模式等。其中的单人维护模式(run level 为 1)是类似于 Windows 中的“安全模式”,在这种情况下,系统不加载复杂的模式从而使系统能够正常启动。在这些模式中最为常见的是 3 或 5,其中本系统中默认的为 5,也就是 X-Windows多用户模式。

 2.执行内容/etc/rc.d/rc.sysinit

开始将 Linux 的主机信息读入 Linux 系统,其内容就是文件“/etc/rc.d/rc.sysinit”中的。

si::sysinit:/etc/rc.d/rc.sysinit

3.启动内核的挂载模块及各运行级的脚本

根据不同的运行级(run  level)加载不同的模块,启动系统服务。

l0:0:wait:/etc/rc.d/rc 0

l1:1:wait:/etc/rc.d/rc 1

 l2:2:wait:/etc/rc.d/rc 2

l3:3:wait:/etc/rc.d/rc 3

l4:4:wait:/etc/rc.d/rc 4

l5:5:wait:/etc/rc.d/rc 5

l6:6:wait:/etc/rc.d/rc 6

ca::ctrlaltdel:/sbin/shutdown -t3 -r now

pf::powerfail:/sbin/shutdown -f -h +2 "Power Failure; System Shutting Down"

pr:12345:powerokwait:/sbin/shutdown -c "Power Restored; Shutdown Cancelled"

1:2345:respawn:/sbin/mingetty tty1

2:2345:respawn:/sbin/mingetty tty2

3:2345:respawn:/sbin/mingetty tty3

4:2345:respawn:/sbin/mingetty tty4

5:2345:respawn:/sbin/mingetty tty5

6:2345:respawn:/sbin/mingetty tty6

x:5:respawn:/etc/X11/prefdm -nodaemon

2.3  Linux 系统服务

INIT 进程的一个重要作用就是启动 Linux 系统服务(也就是运行在后台的守护进程)。Linux    的系统服务包括独立运行的系统服务和 xinet 设定的服务。

2.3.1 独立运行的服务

独立运行的系统服务的启动脚本都放在目录“/etc/rc.d/init.d/”中。

为了指定特定运行级别服务的开启或关闭,系统的各个不同运行级别都有不同的脚本文件,其目录为“/etc/rc.d/rcN.d”,其中的 N 分别对应不用的运行级别。如进入“/rc3.d”目录中的文件可以看到,每个对应的服务都以“K”或“S”开头,其中的K代表关闭(kill),其中的 S 代表启动(start)。

在执行完相应的 rcN.d 目录下的脚本文件后,INIT 最后会执行 rc.local 来启动本地服务,因此,用户若想把某些非系统服务设置为自启动,可以编辑 rc.local 脚本文件,加上相应的执行语句即可。

2.3.2  xinetd 设定的服务

xinetd 管理系统中不经常使用的服务,这些服务程序只有在有请求时才由 xinetd 服务负责启动,一旦运行完毕服务自动结束。xinetd 的配置文件为“/etc/xinetd.conf”,

从该配置文件的最后一行可以看出,xinetd启动“/etc/xinetd.d”为其配置文件目录。再在对应的配置文件目录中可以看到每一个服务的基本配置,如 tftp 服务的配置脚本文件为:

   service tftp

{

socket_type  = dgram//数据包格式

protocol     = udp//使用 UDP 传输

wait  = yes

user  =  root

server =  = /usr/sbin/in.tftpd

server_args  = -s /tftpboot

disable     = yes//不启动

per_source = 11

cps    = 100 2

flags  = IPv4

}

2.3.3  设定服务命令常用方法

Service、hkconfig 、setup 等工具,读者可以自行尝试。

2.4  实验内容

2.4.1在 Linux 下解压常见软件

1.实验目的

通过在 Linux 下安装一个完整的软件(嵌入式 Linux 的必备工具——交叉编译工具),掌握 Linux 常见命令,学会设置环境变量,并同时搭建起了嵌入式 Linux 的交叉编译环境(关于交叉编译的具体概念在本书后面会详细讲解),为今后的实验打下良好的基础。

2.实验内容

在 Linux 中解压3.3.2.tar.bz2,并添加到系统环境变量中去。

2.4.2定制 Linux 系统服务

 1.实验目的

通过定制 Linux 系统服务,进一步理解 Linux 的守护进程,能够更加熟练运用 Linux 操作基本命令,同时也加深对 INIT 进程的了解和掌握。

 2.实验内容查看 Linux 系统服务,并定制其系统服务。

3.实验步骤

查看系统中所有服务及其端口号列表。

命令为:cat /etc/services

思考与练习

1.更改目录的名称,如把/home/ateng 变为/home/kang。

2.若有一文件属性为-rwxr-xrw-,指出其代表什么意思?

3.如何将文件属性变为-rwxrw-r--?

4.下载最新 Linux 源码,并解开至/usr/src 目录下。

5.修改 TELNET、FTP 服务的端口号。


第3章 Shell程序设计

Shell是一个作为用户与Linux系统间接口的程序,它允许用户向操作系统输入需要执行的命令。可以使用下面的命令来查看bash的版本号:

$/bin/sh -version

3.1  Linux 脚本编写基础

3.1.1 语法基本介绍

3.1.1.1 开头

程序必须以下面的行开始(必须放在文件的第一行):

#!/bin/sh

符号#!用来告诉系统它后面的参数是用来执行该文件的程序。在我们的shell程序设计中使用/bin/sh来执行程序。

3.1.1.2 注释

在进行shell编程时,以#开头的句子表示注释,直到这一行的结束。

3.1.1.3 变量

在其他编程语言中您必须使用变量。在shell编程中,所有的变量都由字符串组成,并且您不需要对变量进行声明。要赋值给一个变量,您可以这样写:

#!/bin/sh

#对变量赋值:

a="hello world"

# 现在打印变量a的内容:

echo "A is:"

echo $a

有时候变量名很容易与其他文字混淆,比如:

num=2

echo "this is the $numnd"

这并不会打印出"this is the 2nd",而仅仅打印"this is the ",因为shell会去搜索变量numnd的值,但是这个变量是没有值的。可以使用花括号来告诉shell我们要打印的是num变量:

num=2

echo "this is the ${num}nd"

这将打印:

3.1.1.4 环境变量

export关键字处理过的变量叫做环境变量。具体会创建出哪些变量取决于具体的配置,表中是一些比较重要的环境变量。

如果Shell脚本程序在调用时还带有参数,还会有额外的一些变量。即使根本没有传递任何参数,上表中的环境变量“$#”也依然存在,只不过它的值是0罢了。

3.1.1.5 Shell命令和流程控制

在shell脚本中可以使用三类命令:

(1)linux 命令:

虽然在shell脚本中可以使用任意的linux命令,但是还是有一些相对更常用的命令。这些命令通常是用来进行文件和文字操作的。

常用命令语法及功能

echo "some text": 将文字内容打印在屏幕上

ls: 文件列表

wc –l file wc -w file wc -c file: 计算文件行数,计算文件中的单词数,计算文件中的字符数

cp sourcefile destfile: 文件拷贝

mv oldname newname : 重命名文件或移动文件

rm file: 删除文件

grep 'pattern' file: 在文件内搜索字符串比如:grep 'searchstring' file.txt

cut -b colunm file: 指定欲显示的文件内容范围,并将它们输出到标准输出设备比如:输出每行第5个到第9个字符cut -b5-9 file.txt千万不要和cat命令混淆,这是两个完全不同的命令

cat file.txt: 输出文件内容到标准输出设备(屏幕)上

file somefile: 得到文件类型

read var: 提示用户输入,并将输入赋值给变量

sort file.txt: 对file.txt文件中的行进行排序

uniq: 删除文本文件中出现的行列比如:

expr: 进行数学运算Example: add 2 and 3   expr 2 "+" 3

find: 搜索文件比如:根据文件名搜索find . -name filename -print

basename file: 返回不包含路径的文件名,比如: basename /bin/tux将返回

dirname file: 返回文件所在路径比如:dirname /bin/tux将返回

 

(2) 管道和重定向

这些不是系统命令,但是他们真的很重要。

管道 (|) 将一个命令的输出作为另外一个命令的输入。

grep "hello" file.txt | wc -l

在file.txt中搜索包含有”hello”的行并计算其行数。

在这里grep命令的输出作为wc命令的输入。当然您可以使用多个命令。

重定向:将命令的结果输出到文件,而不是标准输出(屏幕)。

写入文件并覆盖旧文件, 加到文件的尾部,保留旧文件内容。

反短斜线

    使用反短斜线可以将一个命令的输出作为另外一个命令的一个命令行参数。

命令:

find -mtime -1 -type f -print

用来查找过去24小时(-mtime –2则表示过去48小时)内修改过的文件。如果你想将所有查找到的文件打一个包,则可以使用以下脚本:

#!/bin/sh

tar -zcvf lastmod.tar.gz `find -mtime -1 -type f -print`

(3) 流程控制

shell有一组控制结构,而且他们同样与其他程序设计语言很相似。在后面的章节中会详细介绍shell的流程。

 

3.2  管道和重定向

3.2.1 重定向输出

例:

$ls -l > lsoutput.txt

这条命令就是把ls命令的输出保存到lsoutput文件中,这个例子中,我们通过>操作符把标准输出重定向到一个文件。默认情况下,如果该文件已经存在,其内容将被覆盖。我们可以用>>操作符将输出内容附加到一个文件中

$ps >> lsoutput.txt

如果想对标准错误输出进行重定向,需要重定向的文件描述符写在>操作符前面,因为标准错误输出的文件描述符是2,所以2>操作符,当需要丢弃错误信息并阻止他显示在屏幕上时,这个方法很有用。

下面的命令把标准输出和标准错误分别定向到不同的文件中:

kill -hup 1234 >killout.txt  2>killerr.txt

下面这条命令是将标准输出和标准错误都输出到同一个文件中:

kill -l 1234 >killouterr.txt  2>$1

请注意操作符出现的顺序,>killouterr.txt是把标准输出重定向到killouterr.txt中,2>$1是把标准错误输出到与标准输出相同的地方。

如果要丢弃所有的输出信息,可以这样做:

kill -l 1234 >/dev/null

3.2.2 重定向输入

例:

$more < killout.txt

3.2.3 管道

管道操作符|,可以用来连接进程,例如用sort命令对ps命令的输出进行排序。

如果不用管道的话,我们该如果做了?

$ps > psout.txt

$sort psout.txt > pssort.txt

使用管道:

$ps | sort > pssort.out

如果想在屏幕上显示的话:

$ps | sort | more

需要注意的是,如果有一系列的命令需要执行,相应的输出文件是在这一组命令被创建的同时立刻被创建或写入的,所以不要在命令流中重复使用相同的文件名,例如:

$cat mydata.txt | sort | uniq > mydata.txt

那么你将会得到一个空文件,因为你在读取mydata.txt文件之前,就已经覆盖了这个文件的内容。

3.3  shell的语法及控制结构

3.3.1 条件

test 或

以一个例子来说明test的用法

检查一个文件是否存在:

 if test -f file.c

 then

 …

 fi

 

如果使用[]代替test,可以用如下代码表示:

 if [ -f file.c ]

 then

 …

 fi

 test命令的退出码决定是否需要执行后面的代码。

注意:使用[]时,必须要在[符号和被检查的条件之间留出空格,可以把[符号看作和test一样,test和后面的条件之间总是有一个空格。

如果要把then放在和if同一行上,则必须要用一个分号把if和then分开,如下所示:

 if … ;

 ….

 fi

test命令可以使用的条件类型可以归为三类:

字符串比较,算术比较及与文件相关的条件测试,下表描述了这三种类型

 

 

3.3.2 if 语句

 if 语句用来对某个命令的执行结果进行测试,然后根据判断结果有条件的执行一组语句,格式如下:

 if

 then

   statements

 else

   statements

 fi

例:

 #!/bin/sh

 echo  “Is this morning, answer yes or no”

 read timeofday

 if [ $timeofday = “yes”

 then

 echo “Good Morning”

 else

 echo “Good Afternoon”

 fi

 exit 0

3.3.3 elif语句

 elif类似于c语言中的else if语句,那么我们改写一下前一节的代码:

 #!/bin/sh

 echo  “Is this morning, answer yes or no”

 read timeofday

 if [ $timeofday = “yes”

 then

 echo “Good Morning”

 elif [ $timeofday = “no”

 then

 echo “Good Afternoon”

 else

 echo “sorry, $timeofday not recognized. Enter yes or no”

 exit 1

 fi

 exit 0

上面的代码隐藏了一个与变量有关的问题,如果我们在回答的时候,直接按下enter键,那么timeofday的值就是空,if语句就被替换成了

 if [ = “yes”],这不是一个合法的条件,为了避免出现这种情况,我们给timeofday加上“”,

 if [ “$timeofday” = “yes”

 这样就提供了一个合法的测试

 if [ “” = “yes”

3.3.4 for语句

 for结构可用来处理一组循环值

格式如下:

 for variable in value

 do

     statement

 done

 使用固定字符串的for循环:

 #!/bin/sh

 for foo in var bar 43

 do

   echo $foo

 done

 exit 0

如果把第一行的for改成 for foo in “var bar 43”的话,循环只有一次,加上引号就等于告诉shell把引号之间的一切东西都看作是一个字符串,这是在变量中保留空格的一个方法。

使用通配符的for循环:

 #!/bin/sh

 for file in $(ls f*.sh)

 do

     echo $file

 done

 exit 0

 这个例子演示了$(command)的用法,for命令的参数表就来自$()中命令的输出

3.3.5 while语句

 我们来比较两个例子

 #!/bin/sh

 for foo in 1 2 3 4 5 … 30

 do

    echo $foo

 done

 exit 0

在默认情况下,shell的变量都是当作字符串来处理的,所以for在适合于对一系列字符串进行循环处理时使用,但需要执行特定次数时,for就不怎么适用了。

我们来看一下使用while编写的这段代码

#!/bin/sh

 echo “Enter password”

 read passwd

 while [ “$passwd” != “secret”

 do

     echo “try again”

     read passwd

 done

 exit 0

将while结构和数值结合在一起,就可以让特定的命令执行指定的次数,例:

 #!/bin/sh

 foo = 1

 while [ “$foo”

 do

     foo = $(($foo + 1))

 done

 exit 0

3.3.6 until语句

 until 语法如下:

 until condition

 do

     statesment

 done

 until与while循环很相似,只不过while循环是条件满足时,循环继续,而until是条件满足时跳出循环。

 

3.3.7 case语句

 case的语法结构如下:

 case variable in

    pattern [ | pattern ] …) statement;;

    pattern [ | pattern ] …) statement;;
 …

 esac

注意,每个模式都以双分号(;;)结尾,因为你可以在前后模式之间放置多条语句,所以需要使用一个双分号来标记前一个语句的结束和后一个语句的开始

例1:

 #!/bin/sh

 echo “Is it morning?Please answer yes or no”

 read timeofday

 case “$timeofday”

 yes) echo “Good Morning”;;

 no ) echo “Good Afternoon”;;

 y   ) echo “Good Morning”;;

 n   ) echo “Good Afternoon”;;

 *   ) echo “answer not recognized”;;

 esac

 exit 0

例2:

 合并匹配模式

 #!/bin/sh

 echo “Is it morning?Please answer yes or no”

 read timeofday

 case “$timeofday”

 yes | y | YES | Y) echo “Good Morning”;;

 no | n | NO | N)    echo “Good Afternoon”;;

 *   )  echo “answer not recognized”;;

 esac

 exit 0

 

AND列表

语法结构如下:

 statement1 && statement2 && statement3 …

从左开始顺序执行每条命令,如果每条命令返回true,它右边的命令才能执行,直到有一条命令返回false为止。&&作用是用来检查前一条命令的返回值。

例:

 #!/bin/sh

 touch file_one

 rm –f  file_two

 if [ -f file_one ] && echo “hello” && [ -f file_two ] && echo “there”

 then

     echo “in if”

 else

    echo “in else”

 fi

 exit 0

 

 

OR列表

语法结构如下:

statement1 || statement2 || statement3 …

从左开始顺序执行每条命令,如果每条命令返回false,它右边的命令不能执行,直到有一条命令返回true为止。

#!/bin/sh

     rm –f  file_one

 if [ -f file_one ] || echo “hello” || echo “there”

 then

     echo “in if”

 else

    echo “in else”

 fi

 exit 0

3.3.8 函数

定义一个函数,只需简单的写出它的名字,然后是一对空括号,再把有关的语句放在一对花括号中:

function_name()

{

    statements; 

}

例:一个简单的函数

#!/bin/sh

 foo()

 {

    echo “this is a example”

 }

 echo “script starting”

 foo

 echo “script ended”

 exit 0

这个脚本程序从自己的顶部开始执行,当执行到foo()结构时,shell知道定义了一个函数,它会记住foo代表着一个函数并从}字符之后的位置继续执行。当执行到单独的foo时,shell就知道应该去哪开始执行这个刚定义的函数了,当函数执行完后,程序又会回到函数调用的地方,开始执行后面的语句。

当一个函数被调用时,脚本程序的位置参数$*, $@, $#,$1,$2等会被替换为函数的参数,当函数执行完毕后,这些参数会恢复为他们先前的值。

除了参数的传递外,还可以通过return命令让函数返回数值。如果让函数返回字符串,最常用的方法是将字符串保存在一个变量中,而该变量应该可以在函数结束之后被使用。另外,还可以echo一个字符串并捕获其结果,如下:

 foo(){ echo JAY}

 

 result = “$(foo)”

如果要在shell函数中声明局部变量,则要使用local关键字,局部变量的作用域则局限在函数范围内。此外,函数还可访问全局作用范围内的其他shell变量。如果全局变量和局部变量出现了同名,则局部变量会覆盖全局变量,但仅限于函数范围内,例:

#!/bin/sh

 sample_text=“global variable”

 foo()

 {

     local sample_text=“local variable”

     echo “function is executing”

     echo $sample_text

 }

 echo “script starting”

 foo

 echo $sample_text

 echo “script ending”

 exit 0

如果函数没有使用return返回一个返回值的话,那么函数返回的就是最后一条指令执行的退出码。

下面这个例子,演示了函数如何传递参数,及如何返回一个值的:

 #!/bin/sh

 yes_or_no()

 {

     echo “Is your name $* ?”

     while true

     do

         echo “Please enter yes or no”

         read x

         case “$x”

         y | yes ) return 0;;

         n | no ) return 1;;

         * ) echo “Please enter yes or no”

      exac

      done

 }

echo “Original parameters are $*”

 if yes_or_no “$1”

 then

    echo “Hi $1”

  else

    echo “Never mind”

  fi

  exit 0

上面这个程序中,函数foo虽在之前被定义,但并不会立刻执行。程序执行到if语句时,先把$1替换为脚本程序的第一个参数,然后把这个参数传递给函数。那么函数就可以使用传递进来的参数,并将他们保存到$1,$2…这样的位置参数中去,并且函数向调用者返回一个值。在这里要注意的就是,在shell中,返回0值表示成功。

3.3.9 命令

3.3.9.1 break命令

 这个命令在控制条件未满足之前,跳出for/while/until循环。另外还可以为break提供一个参数,表明要跳出的循环的层数,这点跟我们在c语言中的break不一样。不过一般情况下,我们不会这么做,因为它降低了程序的可读性。

 例:

#!/bin/sh

rm -rf fred*

echo > fred1

echo > fred2

mkdir fred3

echo > fred4

for file in fred*

do

if [ -d "$file" ]

then

break;

fi

Done

echo first directory starting fred was $file

rm -rf fred*

exit 0

3.3.9.2 :命令

冒号(:)是一个空命令,它偶尔会被用于逻辑简化,相当于true的一个别名,由于它是内置命令,所以它的运行效率要比true高

 while: 相当于

3.3.9.3 continue命令

 类似于C中的continue,使for/while/until循环跳到下一次循环继续执行,continue也可以附带一个参数,表示希望继续执行的循环嵌套的层数,同样,这个参数也很少用,它会降低程序的可读性。

例:

#!/bin/sh

rm -rf fred*

echo > fred1

echo > fred2

mkdir fred3

echo > fred4

for file in fred*

do 

if [ -d "$file" ] 

then   

   echo skipping the directory $file   

   continue

fi 

echo file is $file

done

rm -rf fred*

exit 0

3.3.9.4 .命令

点(.)命令用来执行当前shell中的命令

如.

 在shell脚本程序中,点命令的作用类似于c语言中的include,因此可以使用点命令将变量和函数定义结合进一个脚本程序中;

例:

1.sh

 #!/bin/sh

 var=“hello 1.sh”

 echo “this is 1.sh script”

2.sh

 #!/bin/sh

 . ./1.sh

 echo “this is 2.sh script”

 exit 0

3.3.9.5 echo命令

最常见的就是如何去掉换行符,linux通常采用如下方法:

 echo -n “hello world”

3.3.9.6 eval命令

eval命令允许对参数进行求值。它是shell的内置命令,通常不会以单独命令的形式存在。

 foo=10

 x=foo

 y=‘$’$x

 echo $y

打印出的结果是$foo

 而

 foo=10

 x=foo

 eval y=‘$’$x

 echo $y

输出是10

当然你也可以用另外一种方法对变量求值

 y=$(($x))

3.3.9.7 exec命令

 exec有两种用法

1) 将当前的shell替换为一个不同的程序

 exec wall “hello world”

 将当前的shell替换为执行wall,exec后面的语句都不会执行了,因为当前的shell已经不复存在了

 2) 修改当前文件描述符

 exec 3< afile

 文件描述符3被打开以便从文件afile中读取数据。

3.3.9.8 exit命令

 exit n命令以退出码n结束运行。

 如果你在退出时没有指定一个退出状态,那么最后一条被执行的命令的状态将被用作返回值。

在shell脚本编程中,0表示成功,1-125是脚本程序使用的错误代码

3.3.9.10 export命令

 export命令将作为它参数的变量导出到子shell中,并使之在子shell中有效。例:

先列出脚本程序export2:

 #!/bin/sh

 echo “$foo”

 echo “$bar”

 然后是脚本程序export1。在这个脚本的末尾,我们调用了export2

 #!/bin/sh

 foo=“the first meta-syntactic variable”

 export bar=“the second meta-syntactic variable”

 ./export2

运行这个脚本程序,输出如下:

 $export 1

 

 the second meta-syntactic variable

第一个空行的出现是因为变量foo在export2中不可用,所

以$foo被赋值为空,echo一个空变量将输出一个空行。

当变量被一个shell导出后,它就可以被该shell调用的任何脚本使用,也可以被后续调用的任何shell使用。如果export2调用了另一个脚本,bar的值对新脚本来说仍然有效

3.3.9.11 printf命令

新版的shell中才提供printf命令,它的语法如下:

 printf “format string” parameter1 parameter2…

下表是它支持的转义序列:

 

主要的转换字符表如下:

 

3.3.9.12 set命令

 set命令的作用是为shell设置参数变量。例:

  #!/bin/sh

  echo the date is $(date)

  set $(date)

  echo the month is $2

  exit 0

这个程序把date命令的输出设置为参数列表,

然后通过位置参数$2获得月份。

3.3.9.13 shift命令

 shift命令把所有的参数变量左移一个位置,使$2变成$1,$3变成$2,依次类推。原来$1的值被丢弃,而$0仍将保持不变。如果调用shift命令时指定了一个数值参数,则表示所有的参数将左移指定的次数。$*,$#,$@等变量也将根据参数变量的新安排做相应的变动。

我们可以像下面这样来扫描所有的位置参数:

 #!/bin/sh

 while [ “$1” != “”

 do

    echo “$1”

    shift

  done

  exit 0

3.3.9.14 trap命令

 trap用于指定在接收到信号后所要采取的行动。

语法格式如下:

 trap command signal

 下表是一些比较重要的信号:

 

例:

#!/bin/sh

trap 'echo hello world' INT

echo "please enter (CTRL-C) to interrupt...“

while true

do

    sleep 1

done

trap -

exit 0

3.3.9.15 unset命令

unset命令的作用是从环境变量中删除变量或函数。这个命令不能删除shell本身定义的只读变量,例:

 #!/bin/sh

 foo=“hello world”

 echo $foo

 unset foo

 echo $foo

 

3.4 正则表达式

为什么要使用正则表达式?

使用shell时,从一个文件中抽取多于一个字符串将会很麻烦。例如,在一个文本中抽取一个词,它的头两个字符是大写的,后面紧跟四个数字。如果不使用某种正则表达式,在shell中将很难实现这个操作。

3.4.1正则表达式的概念

什么是正则表达式?

正则表达式是一些特殊或不很特殊的字符串模式的集合。

为了抽取或获得信息,我们给出抽取操作应遵守的一些规则。这些规则由一些特殊字符或进行模式匹配操作时使用的元字符组成。也可以使用规则字符作为模式中的一部分进行搜寻。例如,A将查询A,x将查找字母x。

系统自带的所有大的文本过滤工具在某种模式下都支持正则表达式的使用,并且还包括一些扩展的元字符集。这里只涉及其中之一,即以字符出现情况进行匹配的表达式,原因是一些系统将这类模式划分为一组形成基本元字符的集合。

1  基本元字符集及其含义

^        只匹配行首

$       只匹配行尾

*        只一个单字符后紧跟*,匹配0个或多个此单字符

[ ]      只匹配[ ]内字符。可以是一个单字符,也可以是字符序列。可以使用-表示[ ]内字符序列范围,如用[ 1 - 5 ]代替[ 1 2 3  4 5 ]

\       只用来屏蔽一个元字符的特殊含义。因为有时在

         s h e l l中一些元字符有特殊含义。\可以使其失

        去应有意义

.       只匹配任意单字符

pattern \ { n \ }       只用来匹配前面p a t t e r n出现次数。n为次数

pattern \ { n,\ }    含义同上,但次数最少为n

pattern \ { n,m \ } 含义同上,但p a t t e r n出现次数在n与m之

           间\ { n,m \ } 只含义同上,但p a t t e r n出现次数在n与m之间

 

3.4.2 二元字符集的使用

1 使用句点匹配单字符

句点“.”可以匹配任意单字符。例如,如果要匹配一个字符串,以beg开头,中间夹一个任意字符,那么可以表示为beg.n,“.”可以匹配字符串头,也可以是中间任意字符。

在ls -l命令中,可以匹配一定权限:

. . . x . . x . . x

此格式匹配用户本身,用户组及其他组成员的执行权限。

假定正在过滤一个文本文件,对于一个有1 0个字符的脚本集,要求前4个字符之后为X C,匹配操作如下:

. . . .X C. . . .

以上例子解释为前4个字符任意,5,6字符为X C,后4个字符也任意.

注意,“.”允许匹配A S C I I集中任意字符,或为字母,或为数字。

 

2 在行首以^匹配字符串或字符序列

^只允许在一行的开始匹配字符或单词。例如,使用ls -l命令,并匹配目录。之所以可以这样做是因为ls -l命令结果每行第一个字符是d,即代表一个目录。

行首前4个字符为comp,匹配操作表示为:

^ comp

假定重新定义匹配模式,行首前4个字符为c o m p,后面紧跟两个任意字符,并以ing结尾,

一种方法为:

^ comp..ing

这些简单的例子,讲述了混合使用正则表达式的概念。

 

3 在行尾以$匹配字符串或字符

可以说$与^正相反,它在行尾匹配字符串或字符, $符号放在匹配单词后。假定要匹配以单词trouble结尾的所有行,操作为:

trouble$

类似的,使用1d $返回每行以1d结尾的所有字符串。

如果要匹配所有空行,执行以下操作:

^ $

具体分析为匹配行首,又匹配行尾,中间没有任何模式,因此为空行。

如果只返回包含一个字符的行,操作如下:

^ . $

不像空白行,在行首与行尾之间有一个模式,代表任意单字符。

如果在行尾匹配单词jet01,操作如下:

jet01 $

 

4 使用*匹配字符串中的单字符或其重复序列

使用此特殊字符匹配任意字符或字符串的重复多次表达式。例如:

compu * t

将匹配字符u一次或多次:

另一个例子:

10133 *

匹配

101333

10133

 

5 使用\屏蔽一个特殊字符的含义

有时需要查找一些字符或字符串,而它们包含了系统指定为特殊字符的一个字符。什么是特殊字符?一般意义上讲,下列字符可以认为是特殊字符:

$ . ' " * [] | 0 \ + ?

假定要匹配包含字符“ .”的各行,而“ .”代表匹配任意单字符的特殊字符,因此需要屏蔽其含义。操作如下:

\ .

上述模式不认为反斜杠后面的字符是特殊字符,而是一个普通字符,即句点。

假定要匹配包含^的各行,将反斜杠放在它前面就可以屏蔽其特殊含义。如下:

\ ^

如果要在正则表达式中匹配以* . pas结尾的所有文件,可做如下操作:

\ * \ . p a s$

即可屏蔽字符*的特定含义。

 

6 使用[]匹配一个范围或集合

使用[ ]匹配特定字符串或字符串集,可以用逗号将括弧内要匹配的不同字符串分开,但并不强制要求这样做(一些系统提倡在复杂的表达式中使用逗号),这样做可以增加模式的可读性。

使用“ -”表示一个字符串范围,表明字符串范围从“ -”左边字符开始,到“ -”右边字

符结束。

如果熟知一个字符串匹配操作,应经常使用[ ]模式。

假定要匹配任意一个数字,可以使用:

[ 0 1 2 3 4 5 6 7 8 9 ]

然而,通过使用“-”符号可以简化操作:

[ 0 - 9 ]

或任意小写字母

[ a - z ]

要匹配任意字母,则使用:

[ A - Z a - z ]

表明从A - Z、a - z的字母范围。

如要匹配任意字母或数字,模式如下:

[ A - Z a - z 0 - 9 ]

在字符序列结合使用中,可以用[ ]指出字符范围。假定要匹配一单词,以s开头,中间有一任意字母,以t结尾,那么操作如下:

s[a-z A-Z]t

上述过程返回大写或小写字母混合的单词,如仅匹配小写字母,可使用:

s [ a - z ] t

如要匹配C o m p u t e r或c o m p u t e r两个单词,可做如下操作:

[ C,c ] o m p u t e r

为抽取诸如S c o u t、s h o u t、b o u g h t等单词,使用下列表达式:

[ou] .*t oaaat obat out

匹配以字母o或u开头,后跟任意一个字符任意次,并以t结尾的任意字母。

也许要匹配所有包含s y s t e m后跟句点的所有单词,这里S可大写或小写。使用如下操作:

[ S,s ] y s t e m \ .

[ ]在指定模式匹配的范围或限制方面很有用。结合使用*与[ ]更是有益,例如[ A - Z a - z ] *将匹配所有单词。

注意^符号的使用,当直接用在第一个括号里,意指否定或不匹配括号里内容。

[^a-zA-Z]

匹配任一非字母型字符,而

[ ^ 0 - 9 ]

匹配任一非数字型字符。

 

7 使用\{\}匹配模式结果出现的次数

使用*可匹配所有匹配结果任意次,但如果只要指定次数,就应使用\ { \ },此模式有三种形式,即:

pattern\{n\} 匹配模式出现n次。

pattern\{n,\} 匹配模式出现最少n次。

pattern\{n,m} 匹配模式出现n到m次之间,n , m为0 - 2 5 5中任意整数。

第一个例子,匹配字母A出现两次,并以B结尾,操作如下:

A \ { 2 \ } B

匹配值为A A B

匹配A至少4次,使用:

A \ { 4 , \ } B

可以得结果A A A A B或A A A A A A A B,但不能为A A A B。

如给出出现次数范围,例如A出现2次到4次之间:

A \ { 2 , 4 \ } B

则结果为A A B、A A A B、A A A A B,而不是A B或A A A A A B等。

假定从下述列表中抽取代码:

1234XC9088

4523XX9001

0011XA9912

9931XC3445

格式如下:前4个字符是数字,接下来是x x,最后4个也是数字,操作如下:

 [ 0 - 9 ] \ { 4 \ }X X[ 0 - 9 ] \ { 4 \ }

具体含义如下:

1) 匹配数字出现4次。

2) 后跟代码x x。

3) 最后是数字出现4次。

结果为

1234XC9088 -no match

4523XX9001 -match

0011XA9912 -no match

9931XC3445 -no match

3.4.3 grep 与正则表达式

grep支持基本正则表达式,也支持其扩展集。g r e p有三种变形,即:

G r e p:标准g r e p命令。

E g r e p:扩展g r e p,支持基本及扩展的正则表达式,但不支持\ q模式范围的应用,与之相对应的一些更加规范的模式,这里也不予讨论。

F g r e p:快速g r e p。允许查找字符串而不是一个模式。不要误解单词f a s t,实际上它与g r e p速度相当。

 

1   grep

grep一般格式为:

grep [选项] 基本正则表达式 [文件]

这里基本正则表达式可为字符串。

1.1双引号引用

在g r e p命令中输入字符串参数时,最好将其用双引号括起来。例如:“m y s t r i n g”。这样做有两个原因,一是以防被误解为s h e l l命令,二是可以用来查找多个单词组成的字符串,例如:

“jet plane”,如果不用双引号将其括起来,那么单词p l a n e将被误认为是一个文件,查询结果将返回“文件不存在”的错误信息。

在调用变量时,也应该使用双引号,诸如: grep “$ myvar”文件名,如果不这样,将没有返回结果。

在调用模式匹配时,应使用单引号。

1.2 grep 选项

常用的grep选项有:

-c 只输出匹配行的计数。

-i 不区分大小写(只适用于单字符)。

-h 查询多文件时不显示文件名。

-l 查询多文件时只输出包含匹配字符的文件名。

-n 显示匹配行及行号。

-s 不显示不存在或无匹配文本的错误信息。

-v 显示不包含匹配文本的所有行。

1.3查询多个文件

如果要在当前目录下所有.doc文件中查找字符串“ sort”,方法如下:

$ grep “sort”

或在所有文件中查询单词“ sort it”

$ grep "sort it" *

现在讲述在文本文件中g r e p选项的用法。

1.4精确匹配

在每个匹配模式中抽取字符串后有一个< Ta b >键,操作如下:

 grep “var<tab>”

< Ta b >表示点击t a b键。

 

2 grep和正则表达式

使用正则表达式使模式匹配加入一些规则,因此可以在抽取信息中加入更多选择。使用正则表达式时最好用单引号括起来,这样可以防止g r e p中使用的专有模式与一些s h e l l命令的特殊方式相混淆。

2.1模式范围

假定要抽取代码为484和483的城市位置,上一章中讲到可以使用[ ]来指定字符串范围,这里用48开始,以3或4结尾,这样抽出484或483。

# grep ’48[34]’

2.2 不匹配首行

如果要抽出记录,使其行首不是4 8,可以在方括号中使用^记号,表明查询在行首开始。

# grep ‘^[^48]’

2.3设置大小写

使用- i开关可以屏蔽月份S e p t的大小写敏感,也可以用另一种方式。这里使用[ ]模式抽取各行包含S e p t和s e p t的所有信息。

 grep -i ‘sep’

2.4 匹配任意字符

如果抽取以L开头,以D结尾的所有代码,可使用下述方法,因为已知代码长度为5个字符:

#grep ‘L…D’

将上述代码做轻微改变,头两个是大写字母,中间两个任意,并以C结尾:

#grep ‘[A-Z][A-Z]..C’

2.5日期查询

一个常用的查询模式是日期查询。先查询所有以5开始以1 9 9 6或1 9 9 8结尾的所有记录。使用模式5 . . 1 9 9 [ 6 , 8 ]。这意味着第一个字符为5,后跟两个点,接着是1 9 9,剩余两个数字是6或8。

#grep ‘5..199[6,8]’

 

查询包含1 9 9 8的所有记录的另外一种方法是使用表达式[ 0 - 9 ] \ { 3 \ } [ 8 ],含义是任意数字重复3次,后跟数字8,虽然这个方法不像上一个方法那么精确,但也有一定作用。

2.6 范围组合

必须学会使用[ ]抽取信息。假定要取得城市代码,第一个字符为任意字符,第二个字符在0到5之间,第三个字符在0到6之间,使用下列模式即可实现。

#grep ‘[0-9][0-5][0-6]’

这里返回很多信息,有想要的,也有不想要的。参照模式,返回结果是正确的,因此这里还需要细化模式,可以以行首开始,使用^符号:

#grep  ^‘[0-9][0-5][0-6]’

 

2.7模式出现机率

抽取包含数字4至少重复出现两次的所有行,方法如下:

#grep ‘4\{2,\}’

有时要查询重复出现次数在一定范围内,比如数字或字母重复出现2到6次,下例匹配数字8重复出现2到6次,并以3结尾:

#grep ‘8\{2,6\}3’

 

2.8使用grep匹配“与”或者“或”模式

gre p命令加- E参数,这一扩展允许使用扩展模式匹配。例如,要抽取城市代码为2 1 9或2 1 6,方法如下:

#grep –E ‘219|216’

 

2.9匹配特殊字符

查询有特殊含义的字符,诸如$ . ' " * [] ^ | \ + ? ,必须在特定字符前加\。假设要查询包含“.”的所有行,脚本如下:

#grep ‘\.’

2.10查询IP地址

查询DNS服务是日常工作之一,这意味着要维护覆盖不同网络的大量I P地址。有时地址IP会超过2000个。如果要查看nnn.nnn网络地址,但是却忘了第二部分中的其余部分,只知有两个句点,例如nnn.nn..。要抽取其中所有nnn.nnn IP地址,使用[0-9] \ {3 \ } \ . [0-9]\{3\} \。含义是任意数字出现3次,后跟句点,接着是任意数字出现3次,后跟句点。

#grep ‘[0-9] \ {3 \ } \ . [0-9]\{3\}\. ’

 

 

3.5 Shell调试技术

3.5.1 在shell脚本中输出调试信息

通过在程序中加入调试语句把一些关键地方或出错的地方的相关信息显示出来是最常见的调试手段。Shell程序员通常使用echo语句输出信息,但仅仅依赖echo语句的输出跟踪信息很麻烦,调试阶段在脚本中加入的大量的echo语句在产品交付时还得再费力一一删除。针对这个问题,本章主要介绍一些如何方便有效的输出调试信息的方法。

 

   1. 使用trap命令

trap命令用于捕获指定的信号并执行预定义的命令。
其基本的语法是:
trap 'command' signal
其中signal是要捕获的信号,command是捕获到指定的信号之后,所要执行的命令。可以用kill –l命令看到系统中全部可用的信号名,捕获信号后所执行的命令可以是任何一条或多条合法的shell语句,也可以是一个函数名。
shell脚本在执行时,会产生三个所谓的“伪信号”,(之所以称之为“伪信号”是因为这三个信号是由shell产生的,而其它的信号是由操作系统产生的),通过使用trap命令捕获这三个“伪信号”并输出相关信息对调试非常有帮助。

表1. shell伪信号

信号名

何时产生

EXIT

从一个函数中退出或整个脚本执行完毕

ERR

当一条命令返回非零状态时(代表命令执行不成功)

DEBUG

脚本中每一条命令执行之前

通过捕获EXIT信号,我们可以在shell脚本中止执行或从函数中退出时,输出某些想要跟踪的变量的值,并由此来判断脚本的执行状态以及出错原因,其使用方法是:
trap 'command' EXIT 或 trap 'command' 0

例:

$ cat -n exp1.sh

1 ERRTRAP()

2 {

3 echo "[LINE:$1] Error: Command or function exited with status $?"

4 }

5 foo()

6 {

7 return 1;

8 }

9 trap 'ERRTRAP $LINENO' ERR

10 abc

11 foo

其输出结果如下:

exp1.sh: line 10: abc: command not found

[LINE:10] Error: Command or function exited with status 127

[LINE:11] Error: Command or function exited with status 1

在调试过程中,为了跟踪某些变量的值,我们常常需要在shell脚本的许多地方插入相同的echo语句来打印相关变量的值,这种做法显得烦琐而笨拙。而通过捕获DEBUG信号,我们只需要一条trap语句就可以完成对相关变量的全程跟踪。

以下是一个通过捕获DEBUG信号来跟踪变量的示例程序:

1 #!/bin/bash

2 trap 'echo “before execute line:$LINENO, a=$a,b=$b,c=$c”' DEBUG

3 a=1

4 if [ "$a" -eq 1 ]

5 then

6 b=2

7 else

8 b=1

9 fi

10 c=3

11 echo "end"

执行结果:

before execute line:3, a=,b=,c=

before execute line:4, a=1,b=,c=

before execute line:6, a=1,b=,c=

before execute line:10, a=1,b=2,c=

before execute line:11, a=1,b=2,c=3

end

 

2 . tee命令

3. 使用"调试钩子"

3.5.2 使用shell的执行选项


第4章 Linux 下的 C 编程基础

4.1  Linux 下 C 语言编程概述

4.1.1 C 语言简单回顾

C 语言有如下特点。

C 语言是“中级语言”。

 C 语言是结构化的语言。

C 语言功能齐全。

C 语言可移植性强。

4.1.2  Linux 下C语言编程环境概述

 Linux 下的 C 语言程序设计与在其他环境中的 C 程序设计一样,主要涉及到编辑器、编译链接器、调试器及项目管理工具。

(1)编辑器

在本书中,着重介绍 Vi。


图  编译过程


(2)编译链接器

编译是指源代码转化生成可执行代码的过程,它所完成工作主要如图 3.1 所示。

(3)调试器

Gdb 是绝大多数 Linux 开发人员所使用的调试器,它可以方便地设置断点、单步跟踪等,足以满足开发人员的需要。

(4)项目管理器

Linux 中的项目管理器“make”有些类似于 Windows 中 Visual C++里的“工程”,它是一种控制编译或者重复编译软件的工具,另外,它还能自动管理软件编译的内容、方式和时机,使程序员能够把精力集中在代码的编写上而不是在源代码的组织上。


4.2  进入 Vi(vi 文件名)

4.2.1  Vi 的模式

Vi 有 3 种模式,分别为命令行模式、插入模式及底行模式能,下面具体进行介绍。

(1)命令行模式

用户在用 Vi 编辑文件时,最初进入的为一般模式。在该模式中可以通过上下移动光标进行“删除字符”或“整行删除”等操作,也可以进行“复制”、“粘贴”等操作,但无法编辑文字。

(2)插入模式

只有在该模式下,用户才能进行文字编辑输入,用户按[ESC]键回到命令行模式。

(3)底行模式

在该模式下,光标位于屏幕的底行。用户可以进行文件保存或退出操作,也可以设置编辑环境,如寻找字符串、列出行号等。

4.2.2  Vi 的基本流程

(1)进入 Vi,即在命令行下键入 Vi hello(文件名)。此时进入的是命令行模式,光标位于屏幕的上方,如图 4.2 所示。

(2)在命令行模式下键入 i 进入到插入模式,如图 4.3 所示。可以看出,在屏幕底部显示有“插入”表示插入模式,在该模式下可以输入文字信息。

图 4.2    进入 Vi 命令行模式


图 4.3    进入 Vi 插入模式

(3)最后,在插入模式中,输入“Esc”,则当前模式转入命令行模式并在底行行中输入“:wq”(存盘退出)进入底行模式,如图 4.4 所示。

图 4.4  进入 Vi 底行模式

4.2.3  Vi 的各模式功能键

(1)命令行模式常见功能键如表 4.1 所示。

表 4.1  Vi 命令行模式功能键


(2)插入模式的功能键只有一个,也就是 Esc 退出到命令行模式。

(3)底行模式常见功能键如表 42 所示。

 

表4.2    Vi 底行模式功能键



4.3  Gcc 编译器

GNU CC(简称为 Gcc)是 GNU 项目中符合 ANSI C 标准的编译系统,能够编译用 C、C++和 Object C 等语言编写的程序。

下表 4.6 是 Gcc 支持编译源文件的后缀及其解释。

表 4.6  Gcc 所支持后缀名解释

4.3.1  Gcc 编译流程解析

如本章开头提到的,Gcc 的编译流程分为了 4 个步骤,分别为:

         预处理(Pre-Processing);

         编译(Compiling);

         汇编(Assembling);

         链接(Linking)。

下面就具体来查看一下 Gcc 是如何完成 4 个步骤的。

首先,有以下 hello.c 源代码:

#include<stdio.h>

int main()

{

      printf("Hello! This is our embedded world!\n");

      return 0;

}


(1)预处理阶段

在该阶段,编译器将上述代码中的 stdio.h 编译进来,并且用户可以使用 Gcc 的选项“-E”进行查看,该选项的作用是让 Gcc 在预处理结束后停止编译过程。


注意 Gcc指令的一般格式为:Gcc [选项]  要编译的文件  [选项] [目标文件]

其中,目标文件可缺省,Gcc 默认生成可执行的文件,命为:编译文件.out

[root@localhost Gcc]#   Gcc –E hello.c   –o hello.i

在此处,选项“-o”是指目标文件,由表 4.6 可知,“.i”文件为已经过预处理的 C 原始

程序。以下列出了 hello.i 文件的部分内容:

 

typedef int (*__gconv_trans_fct) (struct __gconv_step *,

         struct __gconv_step_data *, void *,

         __const unsigned char *,

         __const unsigned char **,

         __const unsigned char *, unsigned char **,

         size_t *);

# 2 "hello.c" 2

int main()

{

 printf("Hello! This is our embedded world!\n");

 return 0;

}


由此可见,Gcc 确实进行了预处理,它把“stdio.h”的内容插入到 hello.i 文件中。

(2)编译阶段

接下来进行的是编译阶段,在这个阶段中,Gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,Gcc把代码翻译成汇编语言。用户可以使用“-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。

 

[root@localhost Gcc]# Gcc –S hello.i  –o hello.s

 

以下列出了 hello.s 的内容,可见 Gcc 已经将其转化为汇编了,感兴趣的读者可以分析一下这一行简单的 C 语言小程序是如何用汇编代码实现的。


  .file  "hello.c"

     .section   .rodata

     .align 4

.LC0:

     .string    "Hello! This is our embedded world!"

     .text

.globl main

     .type main, @function

main:

     pushl %ebp

     movl %esp, %ebp

     subl $8, %esp

andl $-16, %esp

     movl $0, %eax

     addl $15, %eax

     addl $15, %eax

     shrl $4, %eax

     sall $4, %eax

     subl %eax, %esp

     subl $12, %esp

     pushl $.LC0

     call puts

     addl $16, %esp

     movl $0, %eax

     leave

     ret

     .size  main, .-main

     .ident  "GCC: (GNU) 4.0.0 20050519 (Red Hat 4.0.0-8)"

     .section             .note.GNU-stack,"",@progbits


(3)汇编阶段

汇编阶段是把编译阶段生成的“.s”文件转成目标文件,读者在此可使用选项“-c”就可

看到汇编代码已转化为“.o”的二进制目标代码了。如下所示:

[root@localhost Gcc]# Gcc –c hello.s–o hello.o

  (4)链接阶段

在成功编译之后,就进入了链接阶段。在这里涉及到一个重要的概念:函数库。读者可以重新查看这个小程序,在这个程序中并没有定义“printf”的函数实现,且在预编译中包含进的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现“printf”函数的呢?最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,Gcc  会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。

函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为“.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为“.so”,如前面所述的 libc.so.6 就是动态库。Gcc 在编译时默认使用动态库。

完成了链接之后,Gcc 就可以生成可执行文件,如下所示。

[root@localhost Gcc]# Gcc hello.o –o hello

运行该可执行文件,出现正确的结果如下。

 

[root@localhost Gcc]# ./hello

Hello! This is our embedded world!

 

4.3.2 Gcc 编译选项分析

Gcc 有超过 100 个的可用选项,主要包括总体选项、告警和出错选项、优化选项和体系结构相关选项。以下对每一类中最常用的选项进行讲解。

(1)总体选项

Gcc 的总结选项如表 4.7 所示,很多在前面的示例中已经有所涉及。

表 4.7  Gcc 总体选项列表

对于“-c”、“-E”、“-o”、“-S”选项在前一小节中已经讲解了其使用方法,在此主要讲解另外两个非常常用的库依赖选项“-I dir”和“-L dir”。



“-I dir”

正如上表中所述,“-I dir”选项可以在头文件的搜索路径列表中添加 dir 目录。由于 Linux中头文件都默认放到了“/usr/include/”目录下因此,当用户希望添加放置在其他位置的头文件时,就可以通过“-I dir”选项来指定,这样,Gcc 就会到相应的位置查找对应的目录。

比如在“/root/workplace/Gcc”下有两个文件:

 

/*hello1.c*/

#include<my.h>

int main()

{

     printf("Hello!!\n");

     return 0;

}

/*my.h*/

#include<stdio.h>


 

这样,就可在 Gcc 命令行中加入“-I”选项:

[root@localhost Gcc] Gcc hello1.c–I /root/workplace/Gcc/ -o hello1


这样,Gcc 就能够执行出正确结果。

小知识  在 include 语句中,“<>”表示在标准路径中搜索头文件,““””表示在本目录中搜索。故在上例中,可把 hello1.c 的“#include<my.h>”改为“#include “my.h””,就不需要加上“-I”选项了。

  “-L dir”

选项“-L dir”的功能与“-I dir”类似,能够在库文件的搜索路径列表中添加 dir 目录。

例如有程序 hello_sq.c 需要用到目录“/root/workplace/Gcc/lib”下的一个动态库 libateng.so,则只需键入如下命令即可:

 

[root@localhost Gcc] Gcc hello_sq.c–L /root/workplace/Gcc/lib –lateng –o hello_sq


需要注意的是,“-I dir”和“-L dir”都只是指定了路径,而没有指定文件,因此不能在路径中包含文件名。

另外值得详细解释一下的是“-l”选项,它指示 Gcc 去连接库文件 libateng.so。由于在 Linux下的库文件命名时有一个规定:必须以 l、i、b 3 个字母开头。因此在用-l 选项指定链接的库文件名时可以省去 l、i、b  3 个字母。也就是说 Gcc 在对“-lateng”进行处理时,会自动去链接名为 libateng.so 的文件。

(2)告警和出错选项

Gcc 的告警和出错选项如表 4.8 所示。


下面结合实例对这几个告警和出错选项进行简单的讲解。

如有以下程序段:


#include<stdio.h>


void main()

{

long long tmp = 1;

     printf("This is a bad code!\n");

     return 0;

}


这是一个很糟糕的程序,读者可以考虑一下有哪些问题?


“-ansi”


该选项强制 Gcc 生成标准语法所要求的告警信息,尽管这还并不能保证所有没有警告的程序都是符合 ANSI C 标准的。运行结果如下所示:


[root@localhost Gcc]# Gcc -ansi warning.c–o warning

warning.c: 在函数main中:

warning.c:7 警告:在无返回值的函数中,return带返回值

warning.c:4 警告:main的返回类型不是int


可以看出,该选项并没有发现“long long”这个无效数据类型的错误。


“-pedantic”


允许发出  C 标准所列的全部警告信息,同样也保证所有没有警告的程序都是符合ANSI C 标准的。其运行结果如下所示:

 

[root@localhost Gcc]# Gcc   –pedantic warning.c –o warning

warning.c:    在函数“main”中:

warning.c:5 警告:ISO C90 不支持“long long”

warning.c:7 警告:在无返回值的函数中,“return”带返回值

warning.c:4 警告:“main”的返回类型不是“int”

可以看出,使用该选项查看出了“long long”这个无效数据类型的错误。

 

“-Wall”

允许发出 Gcc 能够提供的所有有用的报警信息。该选项的运行结果如下所示:


[root@localhost Gcc]# Gcc             –Wall warning.c–o warning

warning.c:4 警告:main的返回类型不是int

warning.c:            在函数main中:

warning.c:7 警告:在无返回值的函数中,return带返回值

warning.c:5 警告:未使用的变量“tmp

 

使用“-Wall”选项找出了未使用的变量 tmp,但它并没有找出无效数据类型的错误。

另外,Gcc 还可以利用选项对单独的常见错误分别指定警告,有关具体选项的含义感兴趣的读者可以查看 Gcc 手册进行学习。

(3)优化选项

Gcc 可以对代码进行优化,它通过编译选项“-On”来控制优化代码的生成,其中 n 是一个代表优化级别的整数。对于不同版本的 Gcc 来讲,n 的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从 0 变化到 2 或 3。

不同的优化级别对应不同的优化处理工作。如使用优化选项“-O”主要进行线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。使用优化选项“-O2”除了完成所有“-O1”级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。

选项“-O3”则还包括循环展开和其他一些与处理器特性相关的优化工作。

虽然优化选项可以加速代码的运行速度,但对于调试而言将是一个很大的挑战。因为代码在经过优化之后,原先在源程序中声明和使用的变量很可能不再使用,控制流也可能会突然跳转到意外的地方,循环语句也有可能因为循环展开而变得到处都有,所有这些对调试来讲都将是一场噩梦。所以笔者建议在调试的时候最好不使用任何优化选项,只有当程序在最终发行的时候才考虑对其进行优化。

(4)体系结构相关选项

Gcc 的体系结构相关选项如表 4.9 所示。

表 4.9   Gcc 体系结构相关选项列表

这些体系结构相关选项在嵌入式的设计中会有较多的应用,读者需根据不同体系结构将对应的选项进行组合处理。在本书后面涉及到具体实例会有针对性的讲解。

 4.4  Gdb 调试器

调试是所有程序员都会面临的问题。如何提高程序员的调试效率,更好更快地定位程序中的问题从而加快程序开发的进度,是大家共同面对的。就如读者熟知的 Windows 下的一些调试工具,如 VC 自带的如设置断点、单步跟踪等,都受到了广大用户的赞赏。那么,在 Linux下有什么很好的调试工具呢?

本文所介绍的 Gdb 调试器是一款 GNU 开发组织并发布的 UNIX/Linux 下的程序调试工具。虽然,它没有图形化的友好界面,但是它强大的功能也足以与微软的 VC 工具等媲美。

下面就请跟随笔者一步步学习 Gdb 调试器。

4.4.1 Gdb 使用流程

这里给出了一个短小的程序,由此带领读者熟悉一下 Gdb 的使用流程。建议读者能够实际动手操作。

首先,打开 Linux 下的编辑器 Vi 或者 Emacs,编辑如下代码(由于为了更好地熟悉 Gdb的操作,笔者在此使用 Vi 编辑,希望读者能够参见 3.3 节中对 Vi 的介绍,并熟练使用 Vi)。

   

/*test.c*/

#include <stdio.h>

int sum(int m);

int main()

{

      int i,n=0;

      sum(50);

      for(i=1; i<=50; i++)

       {

         n += i;

       }

      printf("The sum of 1-50 is %d \n", n );



}

int sum(int m)

{

         int i,n=0;

         for(i=1; i<=m;i++)

            n += i;

         printf("The sum of 1-m is %d\n", n);

}

 

在保存退出后首先使用 Gcc 对 test.c 进行编译,注意一定要加上选项“-g”,这样编译出的可执行代码中才包含调试信息,否则之后 Gdb 无法载入该可执行文件。

 

[root@localhost Gdb]# gcc -g test.c -o test


  虽然这段程序没有错误,但调试完全正确的程序可以更加了解 Gdb 的使用流程。接下来就启动 Gdb 进行调试。注意,Gdb 进行调试的是可执行文件,而不是如“.c”的源代码,因此,需要先通过 Gcc 编译生成可执行文件才能用 Gdb 进行调试。

 

[root@localhost Gdb]#gdb test

GNU Gdb Red Hat Linux (6.3.0.0-1.21rh)

Copyright 2004 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB.  Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db

library "/lib/libthread_db.so.1".

(gdb)

 

可以看出,在 Gdb 的启动画面中指出了 Gdb 的版本号、使用的库文件等信息,接下来就进入了由“(gdb)”开头的命令行界面了。

(1)查看文件

在 Gdb 中键入“l”(list)就可以查看所载入的文件,如下所示:

注意:

在 Gdb 的命令中都可使用缩略形式的命令,如“l”代便“list”,“b”代表“breakpoint”,“p” 代表“print”等,读者也可使用“help”命令查看帮助信息。


(Gdb) l

1       #include <stdio.h>

2       int sum(int m);

3       int main()

4       {

5             int i,n=0;

6             sum(50);

7             for(i=1; i<=50; i++)

8              {

9                n += i;

10             }

(Gdb)   l

11            printf("The sum of 1~50 is %d \n", n );

12       

13      }

14      int sum(int m)

15      {

16                 int i,n=0;

17                 for(i=1; i<=m;i++)

18                      n += i;

19                 printf("The sum of 1~m is = %d\n", n);

20      }

 

可以看出,Gdb 列出的源代码中明确地给出了对应的行号,这样就可以大大地方便代码的定位。

 2)设置断点

设置断点是调试程序中是一个非常重要的手段,它可以使程序到一定位置暂停它的运行。因此,程序员在该位置处可以方便地查看变量的值、堆栈情况等,从而找出代码的症结所在。

在 Gdb 中设置断点非常简单,只需在“b”后加入对应的行号即可(这是最常用的方式,另外还有其他方式设置断点)。如下所示:

 

(Gdb) b 6

Breakpoint 1 at 0x804846d: file test.c, line 6.



要注意的是,在 Gdb 中利用行号设置断点是指代码运行到对应行之前将其停止,如上例中,代码运行到第 5 行之前暂停(并没有运行第 5 行)。

(3)查看断点情况

在设置完断点之后,用户可以键入“info  b”来查看设置断点情况,在 Gdb 中可以设置多个断点。

 

(Gdb)info b

Num Type           Disp Enb Address    What

1   breakpoint     keep y   0x0804846d in main at test.c:6

 

(4)运行代码

接下来就可运行代码了,Gdb 默认从首行开始运行代码,可键入“r”(run)即可(若想从程序中指定行开始运行,可在 r 后面加上行号)。

 

(Gdb) r

Starting program: /root/workplace/Gdb/test

Reading

Loaded system supplied DSO at 0x5fb000



Breakpoint 1, main () at test.c:6

6                 sum(50);

 

可以看到,程序运行到断点处就停止了。

(5)查看变量值

在程序停止运行之后,程序员所要做的工作是查看断点处的相关变量值。在 Gdb 中只需键入“p”+变量值即可,如下所示:

    

(Gdb) p n

$1 = 0

(Gdb) p i

$2 = 134518440

 

在此处,为什么变量“i”的值为如此奇怪的一个数字呢?原因就在于程序是在断点设置的对应行之前停止的,那么在此时,并没有把“i”的数值赋为零,而只是一个随机的数字。

但变量“n”是在第四行赋值的,故在此时已经为零。

 

小技巧:

    Gdb在显示变量值时都会在对应值之前加上“$N”标记,它是当前变量值的引用标记,所以以后若想再次引用此变量就可以直接写作“$N”,而无需写冗长的变量名。


(6)单步运行

单步运行可以使用命令“n”(next)或“s”(step),它们之间的区别在于:若有函数调用的时候,“s”会进入该函数而“n”不会进入该函数。因此,“s”就类似于 VC 等工具中的“step in”,“n”类似与 VC 等工具中的“step over”。它们的使用如下所示:

(Gdb)   n

The sum of 1-m is 1275

7 for(i=1; i<=50; i++)

(Gdb) s

sum (m=50) at test.c:16

16              int i,n=0;

可见,使用“n”后,程序显示函数 sum 的运行结果并向下执行,而使用“s”后则进入到 sum 函数之中单步运行。

(7)恢复程序运行

在查看完所需变量及堆栈情况后,就可以使用命令“c”(continue)恢复程序的正常运行了。这时,它会把剩余还未执行的程序执行完,并显示剩余程序中的执行结果。以下是之前使用“n”命令恢复后的执行结果:

 

(Gdb) c

Continuing.

The sum of 1-50 is :1275

 

 

Program exited with code 031.

 

可以看出,程序在运行完后退出,之后程序处于“停止状态”。

小知识  在 Gdb 中,程序的运行状态有“运行”、“暂停”和“停止”3 种,其中“暂停”状态为程序遇到了断点或观察点之类的,程序暂时停止运行,而此时函数的地址、函数参数、函数内的局部变量都会被压入“栈”(Stack)中。故在这种状态下可以查看函数的变量值等各种属性。但在函数处于“停止”状态之后,“栈”就会自动撤销,它也就无法查看各种信息了。

4.4.2 Gdb 基本命令

Gdb 的命令可以通过查看 help 进行查找,由于 Gdb 的命令很多,因此 Gdb 的 help 将其分成了很多种类(class),用户可以通过进一步查看相关 class 找到相应命令。如下所示:

 

(gdb) help

List of classes of commands:



aliases -- Aliases of other commands

breakpoints -- Making program stop at certain points

data -- Examining data

files -- Specifying and examining files

internals -- Maintenance commands

Type "help" followed by a class name for a list of commands in that class.

Type "help" followed by command name for full documentation.

Command name abbreViations are allowed if unambiguous.


上述列出了 Gdb 各个分类的命令,注意底部的加粗部分说明其为分类命令。接下来可以具体查找各分类种的命令。如下所示:


(gdb) help data

Examining data.

 

List of commands:

 

 

call -- Call a function in the program

delete display -- Cancel some expressions to be displayed when program stops

delete mem -- Delete memory region

disable display -- Disable some expressions to be displayed when program stops

Type "help" followed by command name for full documentation.

Command name abbreViations are allowed if unambiguous.


 

至此,若用户想要查找 call 命令,就可键入“help call”

 

(gdb) help call

Call a function in the program.

The argument is the function name and arguments, in the notation of the

current working language.  The result is printed and saved in the value

history, if it is not void.


当然,若用户已知命令名,直接键入“help [command]”也是可以的。

Gdb 中的命令主要分为以下几类:工作环境相关命令、设置断点与恢复命令、源代码查

看命令、查看运行数据相关命令及修改运行参数命令。以下就分别对这几类的命令进行讲解。

1.工作环境相关命令

Gdb 中不仅可以调试所运行的程序,而且还可以对程序相关的工作环境进行相应的设定,甚至还可以使用 shell 中的命令进行相关的操作,其功能极其强大。表 4.10 所示为 Gdb 常见工作环境相关命令。

表 4.10     Gdb 工作环境相关命令

 

2.设置断点与恢复命令

Gdb 中设置断点与恢复的常见命令如表4.11 所示。


表 4.11  Gdb 设置断点与恢复相关

 

由于设置断点在 Gdb 的调试中非常重要,所以在此再着重讲解一下 Gdb 中设置断点的方法。

Gdb 中设置断点有多种方式:其一是按行设置断点,设置方法在3.5.1节已经指出,在此就不重复了。另外还可以设置函数断点和条件断点,在此结合上一小节的代码,具体介绍后两种设置断点的方法。

① 函数断点

Gdb 中按函数设置断点只需把函数名列在命令“b”之后,如下所示:

 

(gdb) b sum

Breakpoint 1 at 0x80484ba: file test.c, line 16.

(gdb) info b

Num Type           Disp Enb Address    What

1   breakpoint     keep y   0x080484ba in sum at test.c:16

 

 

要注意的是,此时的断点实际是在函数的定义处,也就是在 16 行处(注意第 16 行还未执行)。

② 条件断点

Gdb 中设置条件断点的格式为:b  行数或函数名  if  表达式。具体实例如下所示:

 

(gdb) b 8 if i==10

Breakpoint 1 at 0x804848c: file test.c, line 8.

(gdb) info b

Num Type           Disp Enb Address    What

1   breakpoint     keep y   0x0804848c in main at test.c:8

 stop only if i == 10

(gdb) r

Starting program: /home/yul/test

The sum of 1-m is 1275

 

 

Breakpoint 1, main () at test.c:9

9               n += i;

(gdb) p i

$1 = 10

 

 

可以看到,该例中在第 8 行(也就是运行完第 7 行的 for 循环)设置了一个“i==10”的条件断点,在程序运行之后可以看出,程序确实在 i 为 10 时暂停运行。

3.Gdb 中源码查看相关命令

在 Gdb 中可以查看源码以方便其他操作,它的常见相关命令如表 4.12 所示。

 

表 4.12                Gdb 源码查看相关相关命令



4.Gdb 中查看运行数据相关命令


Gdb 中查看运行数据是指当程序处于“运行”或“暂停”状态时,可以查看的变量及表达式的信息,其常见命令如表 4.13 所示:

表 4.13    Gdb 查看运行数据相关


 

5.Gdb 中修改运行参数相关命令


Gdb 还可以修改运行时的参数,并使该变量按照用户当前输入的值继续运行。它的设置方法为:在单步执行的过程中,键入命令“set 变量=设定值”。这样,在此之后,程序就会按照该设定的值运行了。下面,笔者结合上一节的代码将 n 的初始值设为 4,其代码如下所示:


(Gdb) b 7

Breakpoint 5 at 0x804847a: file test.c, line 7.

(Gdb) r

Starting program: /home/yul/test

The sum of 1-m is 1275



Breakpoint 5, main () at test.c:7

7  for(i=1; i<=50; i++)

(Gdb)   set n=4

(Gdb) c

Continuing.

The sum of 1-50 is 1279

 

 

Program exited with code 031.

可以看到,最后的运行结果确实比之前的值大了 4。

Gdb 的使用切记点:在 Gcc 编译选项中一定要加入“-g”。只有在代码处于“运行”或“暂停”状态时才能查看变量值。设置断点后程序在指定行之前停止。

4.5  Make 工程管理器

所谓工程管理器,顾名思义,是指管理较多的文件的。读者可以试想一下,有一个上百个文件的代码构成的项目,如果其中只有一个或少数几个文件进行了修改,按照之前所学的Gcc  编译工具,就不得不把这所有的文件重新编译一遍,因为编译器并不知道哪些文件是最近更新的,而只知道需要包含这些文件才能把源代码编译成可执行文件,于是,程序员就不能不再重新输入数目如此庞大的文件名以完成最后的编译工作。

但是,请读者仔细回想一下本书在3.1.2节中所阐述的编译过程,编译过程是分为编译、汇编、链接不同阶段的,其中编译阶段仅检查语法错误以及函数与变量的声明是否正确声明了,在链接阶段则主要完成是函数链接和全局变量的链接。因此,那些没有改动的源代码根本不需要重新编译,而只要把它们重新链接进去就可以了。所以,人们就希望有一个工程管理器能够自动识别更新了的文件代码,同时又不需要重复输入冗长的命令行,这样,Make工程管理器也就应运而生了。

实际上,Make 工程管理器也就是个“自动编译管理器”,这里的“自动”是指它能够根据文件时间戳自动发现更新过的文件而减少编译的工作量,同时,它通过读入 Makefile 文件的内容来执行大量的编译工作。用户只需编写一次简单的编译语句就可以了。它大大提高了实际项目的工作效率,而且几乎所有 Linux 下的项目编程均会涉及它,希望读者能够认真学习本节内容。


4.5.1  Makefile 基本结构

 Makefile 是 Make 读入的惟一配置文件,因此本节的内容实际就是讲述 Makefile 的编写规则。在一个 Makefile 中通常包含如下内容:

 需要由 make 工具创建的目标体(target),通常是目标文件或可执行文件;

 要创建的目标体所依赖的文件(dependency_file);

 创建每个目标体时需要运行的命令(command)。

 

它的格式为:

target: dependency_files

       command


例如,有两个文件分别为 hello.c 和 hello.h,创建的目标体为 hello.o,执行的命令为 gcc编译指令:gcc –c hello.c,那么,对应的 Makefile 就可以写为:

 

#The simplest example

hello.o: hello.c hello.h

                 gcc–c hello.c –o hello.o(前面是一个制表位tab)


接着就可以使用 make 了。使用 make 的格式为:make  target,这样 make 就会自动读入Makefile(也可以是首字母小写 makefile)并执行对应 target 的 command 语句,并会找到相应的依赖文件。如下所示:

 

[root@localhost makefile]# make hello.o

gcc–c hello.c –o hello.o

[root@localhost makefile]# ls

hello.c  hello.h  hello.o  Makefile


可以看到,Makefile 执行了“hello.o”对应的命令语句,并生成了“hello.o”目标体。

注意  在 Makefile 中的每一个 command 前必须有“Tab”符,否则在运行 make 命令时会出错。

 

4.5.2 Makefile 变量

上面示例的 Makefile 在实际中是几乎不存在的,因为它过于简单,仅包含两个文件和一个命令,在这种情况下完全不必要编写 Makefile 而只需在 Shell 中直接输入即可,在实际中使用的 Makefile 往往是包含很多的文件和命令的,这也是 Makefile 产生的原因。下面就可给出稍微复杂一些的 Makefile 进行讲解:

ateng:kang.o yul.o

Gcc kang.o bar.o -o myprog

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 中有 3 个目标体(target),分别为 ateng、kang.o 和 yul.o,其中第一个目标体的依赖文件就是后两个目标体。如果用户使用命令“make ateng”,则 make 管理器就是找到 ateng 目标体开始执行。

这时,make 会自动检查相关文件的时间戳。首先,在检查“kang.o”、“yul.o”和“ateng”3 个文件的时间戳之前,它会向下查找那些把“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”和“ateng”3 个文件,只要文件“kang.o”或“yul.o”中的任比文件时间戳比“ateng”新,则第二行命令就会被执行。这样,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

ateng : $(OBJS)

       $(CC) $(OBJS) -o ateng

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 都会出现的变量,其中部分有默认值,也就是常见的设定值,当然用户可以对其进行修改。

预定义变量包含了常见编译器、汇编器的名称及其编译选项。表   4.14   列出了Makefile中常见预定义变量及其部分默认值。

表 4.14   Makefile 中常见预定义变量

 

可以看出,上例中的 CC 和 CFLAGS 是预定义变量,其中由于 CC 没有采用默认值,因此,需要把“CC=Gcc”明确列出来。

由于常见的 Gcc 编译语句中通常包含了目标文件和依赖文件,而这些文件在 Makefile 文件中目标体的一行已经有所体现,因此,为了进一步简化 Makefile 的编写,就引入了自动变量。自动变量通常可以代表编译语句中出现目标文件和依赖文件等,并且具有本地含义(即下一语句中出现的相同变量代表的是下一语句的目标文件和依赖文件)。

表 4.15   列出了Makefile 中常见自动变量。

 

 

自动变量的书写比较难记,但是在熟练了之后会非常的方便,请读者结合下例中的自动变量改写的 Makefile 进行记忆。

 

OBJS = kang.o yul.o

CC = Gcc

CFLAGS = -Wall -O -g

ateng : $(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 中定义了相同名称的变量,那么用户自定义变量将会覆盖同名的环境变量。

4.5.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

ateng : $(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”文件生成。

 

表4.16 给出了常见的隐式规则目录


  2.模式规则

     模式规则是用来定义相同处理规则的多个文件的。它不同于隐式规则,隐式规则仅仅能够用    make 默认的变量来进行操作,而模式规则还能引入用户自定义变量,为多个文件建立相同的规则,从而简化 Makefile 的编写。

模式规则的格式类似于普通规则,这个规则中的相关文件前必须用“%”标明。使用模式规则修改后的 Makefile 的编写如下:

 

OBJS = kang.o yul.o

CC = Gcc

CFLAGS = -Wall -O -g

ateng : $(OBJS)

      $(CC) $^ -o   $@

%.o : %.c

      $(CC) $(CFLAGS) -c $< -o   $@

 

4.5.4 Make 管理器的使用

  使用 Make 管理器非常简单,只需在 make 命令的后面键入目标名即可建立指定的目标,如果直接运行 make,则建立 Makefile 中的第一个目标。

此外 make 还有丰富的命令行选项,可以完成各种不同的功能。下表4.17 列出了常用的make 命令行选项。

                 表4-17   Make 的命令行选项



4.6 使用 autotools

在上一小节,读者已经了解到了 make 项目管理器的强大功能。的确,Makefile  可以帮助 make 完成它的使命,但要承认的是,编写 Makefile 确实不是一件轻松的事,尤其对于一个较大的项目而言更是如此。那么,有没有一种轻松的手段生成 Makefile 而同时又能让用户享受 make 的优越性呢?本节要讲的 autotools 系列工具正是为此而设的,它只需用户输入简单的目标文件、依赖文件、文件目录等就可以轻松地生成 Makefile 了,这无疑是广大用户的所希望的。另外,这些工具还可以完成系统配置信息的收集,从而可以方便地处理各种移植性的问题。也正是基于此,现在 Linux 上的软件开发一般都用 autotools 来制作 Makefile,读者在后面的讲述中就会了解到。

4.6.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”原型。如下所示:

 

[root@localhost automake]# autoscan

autom4te: configure.ac: no such file or directory

autoscan: /usr/bin/autom4te failed with exit status: 1

[root@localhost automake]# ls

autoscan.log  configure.scan  hello.c


由上述代码可知 autoscan 首先会尝试去读入“configure.ac”(同 configure.in 的配置文件)文件,此时还没有创建该配置文件,于是它会自动生成一个“configure.in”的原型文件“configure.scan”。

 

2.autoconf

configure.in 是 autoconf 的脚本配置文件,它的原型文件“configure.scan”如下所示:


# -*- Autoconf -*-

# Process this file with autoconf to produce a configure script.

AC_PREREQ(2.59)

#The next one is modified by ateng

#AC_INIT(FULL-PACKAGE-NAME,VERSION,BUG-REPORT-ADDRESS)

AC_INIT(hello,1.0)

# The next one is added by ateng

AM_INIT_AUTOMAKE(hello,1.0)

AC_CONFIG_SRCDIR([hello.c])

AC_CONFIG_HEADER([config.h])

# Checks for programs.

AC_PROG_CC

# Checks for libraries.

# Checks for header files.

# Checks for typedefs, structures, and compiler characteristics.

# Checks for library functions.

AC_CONFIG_FILES([Makefile])

AC_OUTPUT


下面对这个脚本文件进行解释。

 以“#”号开始的行为注释。

    AC_PREREQ 宏声明本文件要求的 autoconf 版本,如本例使用的版本 2.59。

 AC_INIT 宏用来定义软件的名称和版本等信息,在本例中省略了BUG-REPORT-ADDRESS,一般为作者的 E-mail。

    AM_INIT_AUTOMAKE 是笔者另加的,它是 automake 所必备的宏,也同前面一样,PACKAGE 是所要产生软件套件的名称,VERSION 是版本编号。

    AC_CONFIG_SRCDIR 宏用来侦测所指定的源码文件是否存在,来确定源码目录的有效性。在此处为当前目录下的 hello.c。

    AC_CONFIG_HEADER 宏用于生成 config.h 文件,以便 autoheader 使用。

    AC_CONFIG_FILES 宏用于生成相应的 Makefile 文件。

 中间的注释间可以添加分别用户测试程序、测试函数库、测试头文件等宏定义。

接下来首先运行 aclocal,生成一个“aclocal.m4”文件,该文件主要处理本地的宏定义。

如下所示:

 [root@localhost automake]# aclocal

再接着运行 autoconf,生成“configure”可执行文件。如下所示:

[root@localhost automake]#  autoconf

[root@localhost automake]#  ls

aclocal.m4  autom4te.cache  autoscan.log  configure  configure.in  hello.c

 

3.autoheader

接着使用 autoheader 命令,它负责生成 config.h.in 文件。该工具通常会从“acconfig.h”文件中复制用户附加的符号定义,因此此处没有附加符号定义,所以不需要创建“acconfig.h”文件。如下所示:

[root@localhost automake]# autoheader

 

4.automake

这一步是创建 Makefile 很重要的一步,automake 要用的脚本配置文件是 Makefile.am,用户需要自己创建相应的文件。之后,automake 工具转换成 Makefile.in。在该例中,笔者创建的文件为 Makefile.am 如下所示:

AUTOMAKE_OPTIONS=foreign

bin_PROGRAMS= hello

hello_SOURCES= hello.c

下面对该脚本文件的对应项进行解释。

 其中的 AUTOMAKE_OPTIONS 为设置 automake 的选项。由于 GNU(在第 1 章中已经有所介绍)对自己发布的软件有严格的规范,比如必须附带许可证声明文件 COPYING 等,否则 automake 执行时会报错。automake 提供了 3 种软件等级:foreign、gnu 和 gnits,让用户选择采用,默认等级为 gnu。在本例使用 foreign 等级,它只检测必须的文件。

    bin_PROGRAMS 定义要产生的执行文件名。如果要产生多个执行文件,每个文件名用空格隔开。

    hello_SOURCES  定义“hello”这个执行程序所需要的原始文件。如果“hello”这个程序是由多个原始文件所产生的,则必须把它所用到的所有原始文件都列出来,并用空格隔开。例如:若目标体“hello”需要“hello.c”、“ateng.c”、“hello.h”三个依赖文件,则定义hello_SOURCES=hello.c ateng.c hello.h。要注意的是,如果要定义多个执行文件,则对每个执行程序都要定义相应的 file_SOURCES。

接 下 来 可 以 使 用 automake   对 其 生 成 “ configure.in ” 文 件 , 在 这 里 使 用 选 项“—adding-missing”可以让 automake 自动添加有一些必需的脚本文件。如下所示:


[root@localhost automake]#  automake --add-missing

configure.in: installing './install-sh'

configure.in: installing './missing'

Makefile.am: installing 'depcomp'

[root@localhost automake]#  ls

aclocal.m4      autoscan.log    configure.in  hello.c     Makefile.am  missing


autom4te.cache  configure     depcomp    install-sh  Makefile.in  config.h.in

可以看到,在 automake 之后就可以生成 configure.in 文件。


5.运行 configure

在这一步中,通过运行自动配置设置文件configure,把Makefile.in  变成了最终的Makefile。如下所示:

 

[root@localhost automake]#  ./configure

checking for a BSD-compatible install... /usr/bin/install -c

checking whether build enVironment is sane... yes

checking for gawk... gawk

checking whether make sets $(MAKE)... yes

checking for Gcc... Gcc

checking for C compiler default output file name... a.out

checking whether the C compiler works... yes

checking whether we are cross compiling... no

checking for suffix of executables...

checking for suffix of object files... o

checking whether we are using the GNU C compiler... yes

checking whether Gcc accepts -g... yes

checking for Gcc option to accept ANSI C... none needed

checking for style of include used by make... GNU

checking dependency style of Gcc... Gcc3

configure: creating ./config.status

config.status: creating Makefile

config.status: executing depfiles commands



可以看到,在运行 configure 时收集了系统的信息,用户可以在 configure 命令中对其进行 方 便 地 配 置 。 在./configure 的自 定义 参 数有 两 种, 一 种是 开关 式 (    或--disable-XXX),另一种是开放式,即后面要填入一串字符(--with-XXX=yyyy)参数。读者可以自行尝试其使用方法。另外,读者可以查看同一目录下的“config.log”文件,以方便调试之用。

到此为止,makefile 就可以自动生成了。回忆整个步骤,用户不再需要定制不同的规则,而只需要输入简单的文件及目录名即可,这样就大大方便了用户的使用。autotools 生成Makefile 流程图如图 4.9 所示。

 4.9    autotools 生成 Makefile 流程图

 

4.6.2  使用 autotools 所生成的

autotools 生成的 Makefile 除具有普通的编译功能外,还具有以下主要功能(感兴趣的读者可以查看这个简单的 hello.c 程序的 makefile)。

 

1.make

键入 make 默认执行“make all”命令,即目标体为 all,其执行情况如下所示:

[root@localhost automake]# make

if  Gcc  -DPACKAGE_NAME=\"\"  -DPACKAGE_TARNAME=\"\"  -DPACKAGE_VERSION=\"\"

-DPACKAGE_STRING=\"\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\"  -I. -I.     -g -O2 -MT hello.o -MD -MP -MF ".deps/hello.Tpo" -c -o hello.o hello.c; \

then mv -f ".deps/hello.Tpo" ".deps/hello.Po"; else rm -f ".deps/hello.Tpo";

exit 1; fi

Gcc  -g -O2   -o hello  hello.o


此时在本目录下就生成了可执行文件“hello”,运行“./hello”能出现正常结果,如下

所示:


[root@localhost automake]#  ./hello

Hello!Autoconf!

 

2.make install

此时,会把该程序安装到系统目录中去,如下所示:


[root@localhost automake]# make install

if  Gcc  -DPACKAGE_NAME=\"\"  -DPACKAGE_TARNAME=\"\"  -DPACKAGE_VERSION=\"\"

-DPACKAGE_STRING=\"\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE=\"hello\" -DVERSION=\"1.0\"  -I. -I.     -g -O2 -MT hello.o -MD -MP -MF ".deps/hello.Tpo" -c -o hello.o hello.c; \

then mv -f ".deps/hello.Tpo" ".deps/hello.Po"; else rm -f ".deps/hello.Tpo";

exit 1; fi

Gcc  -g -O2   -o hello  hello.o

make[1]: Entering directory '/root/workplace/automake'

test -z "/usr/local/bin" || mkdir -p -- "/usr/local/bin"

  /usr/bin/install -c 'hello' '/usr/local/bin/hello'

make[1]: Nothing to be done for 'install-data-am'.

make[1]: LeaVing directory '/root/workplace/automake'


此时,若直接运行 hello,也能出现正确结果,如下所示:

[root@localhost automake]#  hello

Hello!Autoconf!

 

3.make clean

此时,make 会清除之前所编译的可执行文件及目标文件(object file, *.o),如下所示:

[root@localhost automake]#  make clean

test -z "hello" || rm -f hello

rm -f *.o

 

4.make dist

此时,make 将程序和相关的文档打包为一个压缩文档以供发布,如下所示:

[root@localhost automake]#  make dist

[root@localhost automake]# ls hello-1.0-tar.gz

hello-1.0-tar.gz

 

可见该命令生成了一个 hello-1.0-tar.gz 的压缩文件。

由上面的讲述读者不难看出,autotools 确实是软件维护与发布的必备工具,鉴于此,如今 GUN 的软件一般都是由 automake 来制作的。

    想一想    对于 automake 制作的这类软件,应如何安装呢?

 

4.7  实验内容

4.7.1  Vi 使用练习

1.实验目的

通过指定指令的 Vi 操作练习,使读者能够熟练使用 Vi 中的常见操作,并且熟悉 Vi 的 3种模式,如果读者能够熟练掌握实验内容中所要求的内容,则表明对 Vi 的操作已经很熟练了。

 

2.实验内容

(1)在“/root”目录下建一个名为“/Vi”的目录。

(2)进入“/Vi”目录。

(3)将文件“/etc/inittab”复制到“/Vi”目录下。

(4)使用 Vi 打开“/Vi”目录下的 inittab。

(5)设定行号,指出设定 initdefault(类似于“id:5:initdefault”)的所在行号。

(6)将光标移到该行。

(7)复制该行内容。

(8)将光标移到最后一行行首。

(9)粘贴复制行的内容。

(10)撤销第 9 步的动作。

(11)将光标移动到最后一行的行尾。

(12)粘贴复制行的内容。

(13)光标移到“si::sysinit:/etc/rc.d/rc.sysinit”。

(14)删除该行。

(15)存盘但不退出。

(16)将光标移到首行。

(17)插入模式下输入“Hello,this is Vi world!”。

(18)返回命令行模式。

(19)向下查找字符串“0:wait”。

(20)再向上查找字符串“halt”。

(21)强制退出 Vi,不存盘。

分别指出每个命令处于何种模式下?


3.实验步骤

(1)mkdir /root/Vi

(2)cd /root/Vi

(3)cp /etc/inittab ./

(4)Vi ./inittab

(5):set nu(底行模式)

(6)17<enter>(命令行模式)

(7)yy

(8)G

(9)p

(10)u

(11)$

(12)p

(13)21G

(14)dd

(15):w(底行模式)

(16)1G

(17)i 并输入“Hello,this is Vi world!”(插入模式)

(18)Esc

(19)/0:wait(命令行模式)

(20)?halt

(21):q!(底行模式)


4.实验结果

该实验最后的结果只对“/root/inittab”增加了一行复制的内容:“id:5:initdefault”。

4.7.2  用 Gdb 调试有问题的程序


1.实验目的

通过调试一个有问题的程序,使读者进一步熟练使用 Vi 操作,而且熟练掌握 Gcc 编译

命令及 Gdb 的调试命令,通过对有问题程序的跟踪调试,进一步提高发现问题和解决问题的

能力。这是一个很小的程序,只有 35 行,希望读者认真调试。

 

2.实验内容

(1)使用 Vi 编辑器,将以下代码输入到名为 greet.c 的文件中。此代码的原意为输出倒序 main 函数中定义的字符串,但结果显示没有输出。代码如下所示:

#include <stdio.h>

int display1(char *string);

int display2(char *string);


int main ()

{

       char string[] = "Embedded Linux";

       display1 (string);

       display2 (string);

}

int display1 (char *string)

{

       printf ("The original string is %s \n", string);

}

int display2 (char *string1)

{

       char *string2;

       int size,i;

       size = strlen (string1);

       string2 = (char *) malloc (size + 1);

       for (i = 0; i < size; i++)

               string2[size - i] = string1[i];

       string2[size+1] = ' ';

       printf("The string afterward is %s\n",string2);

}

 

(2)使用 Gcc 编译这段代码,注意要加上“-g”选项以方便之后的调试。

(3)运行生成的可执行文件,观察运行结果。

(4)使用 Gdb 调试程序,通过设置断点、单步跟踪,一步步找出错误所在。

(5)纠正错误,更改源程序并得到正确的结果。

 

3.实验步骤

(1)在工作目录上新建文件 greet.c,并用 Vi 启动:vi greet.c。

(2)在 Vi 中输入以上代码。

(3)在 Vi 中保存并退出:wq。

(4)用 Gcc 编译:gcc -g greet.c -o greet。

(5)运行 greet:./greet,输出为:

The original string is Embedded Linux

The string afterward is


可见,该程序没有能够倒序输出。

(6)启动 Gdb 调试:gdb greet。

(7)查看源代码,使用命令“l”。

(8)在 30 行(for 循环处)设置断点,使用命令“b 30”。

(9)在 33 行(printf 函数处)设置断点,使用命令“b 33”。

(10)查看断点设置情况,使用命令“info b”。

(11)运行代码,使用命令“r”。

(12)单步运行代码,使用命令“n”。

(13)查看暂停点变量值,使用命令“p string2[size - i]”。

(14)继续单步运行代码数次,并使用命令查看,发现 string2[size-1]的值正确。

(15)继续程序的运行,使用命令“c”。

(16)程序在 printf 前停止运行,此时依次查看 string2[0]、string2[1]…,发现 string[0]没有被正确赋值,而后面的复制都是正确的,这时,定位程序第 31 行,发现程序运行结果错误的原因在于“size-1”。由于 i 只能增到“size-1”,这样 string2[0]就永远不能被赋值而保持 NULL,故输不出任何结果。

(17)退出 Gdb,使用命令 q。

(18)重新编辑 greet.c,把其中的“string2[size - i] = string1[i]”改为“string2[size – i - 1] =

string1[i];”即可。

(19)使用 Gcc 重新编译:gcc -g greet.c -o greet。

(20)查看运行结果:./greet

The original string is Embedded Linux

The string afterward is xuniL deddedbmE

这时,输入结果正确。


4.实验结果

将原来有错的程序经过 Gdb 调试,找出问题所在,并修改源代码,输出正确的倒序显示字符串的结果。

4.7.3  编写包含多文件的


1.实验目的

通过对包含多文件的   Makefile   的编写,熟悉各种形式的   Makefile,并且进一步加深对Makefile 中用户自定义变量、自动变量及预定义变量的理解。

 

2.实验过程

(1)用 Vi 在同一目录下编辑两个简单的 Hello 程序,如下所示:

(2)仍在同一目录下用 Vi 编辑 Makefile,且不使用变量替换,用一个目标体实现(即直接将 hello.c 和 hello.h 编译成 hello 目标体)。然后用 make 验证所编写的Makefile 是否正确。

(3)将上述 Makefile 使用变量替换实现。同样用 make 验证所编写的 Makefile 是否正确。

(4)用编辑另一 Makefile,取名为 Makefile1,不使用变量替换,但用两个目标体实现(也就是首先将 hello.c 和 hello.h 编译为 hello.o,再将 hello.o 编译为 hello),再用 make 的“-f”选项验证这个 Makefile1 的正确性。

(5)将上述 Makefile1 使用变量替换实现。

 

3.实验步骤

(1)用 Vi 打开上述两个代码文件“hello.c”和“hello.h”。

(2)在 shell 命令行中用 Gcc 尝试编译,使用命令:“Gcc hello.c –o hello”,并运行

可执行文件查看结果。

(3)删除此次编译的可执行文件:rm hello。

(4)用 Vi 编辑 Makefile,如下所示:

hello:hello.c hello.h

       Gcc hello.c -o hello

 

(5)退出保存,在 shell 中键入:make,查看结果。

(6)再次用 Vi 打开 Makefile,用变量进行替换,如下所示:

OBJS :=hello.o

CC :=Gcc

hello:$(OBJS)

       $(CC) $^ -o $@

 

(7)退出保存,在 shell 中键入 make,查看结果。

(8)用 Vi 编辑 Makefile1,如下所示:

 

 

hello:hello.o

       Gcc hello.o -o hello

hello.o:hello.c hello.h

       Gcc -c hello.c -o hello.o

 

(9)退出保存,在 shell 中键入:make -f Makefile1,查看结果。

(10)再次用 Vi 编辑 Makefile1,如下所示:

OBJS1 :=hello.o

OBJS2 :=hello.c hello.h

CC :=Gcc

hello:$(OBJS1)

       $(CC) $^ -o $@

$(OBJS1):$(OBJS2)

       $(CC) -c $< -o $@

 

在这里请注意区别“$^”和“$<”。

(11)退出保存,在 shell 中键入 make -f Makefile1,查看结果。

 

4.实验结果

各种不同形式的 makefile 都能完成其正确的功能。

4.7.4  使用 autotools 生成包含多文件的


1.实验目的

通过使用 autotools 生成包含多文件的 Makefile,进一步掌握 autotools 的正确使用方法。

同时,掌握 Linux 下安装软件的常用方法。

 

2.实验过程

(1)在原目录下新建文件夹 auto。

(2)利用上例的两个代码文件“hello.c”和“hello.h”,并将它们复制到该目录下。

(3)使用 autoscan 生成 configure.scan。

(4)编辑 configure.scan,修改相关内容,并将其重命名为 configure.in。

(5)使用 aclocal 生成 aclocal.m4。

(6)使用 autoconf 生成 configure。

(7)使用 autoheader 生成 config.in.h。

(8)编辑 Makefile.am。

(9)使用 automake 生成 Makefile.in。

(10)使用 configure 生成 Makefile。

(11)使用 make 生成 hello 可执行文件,并在当前目录下运行 hello 查看结果。

(12)使用 make install 将 hello 安装到系统目录下,并运行,查看结果。

(13)使用 make dist 生成 hello 压缩包。

(14)解压 hello 压缩包。

(15)进入解压目录。

(16)在该目录下安装 hello 软件。

 

3.实验步骤

(1)mkdir ./auto。

(2)cp hello.* ./auto(假定原先在“hello.c”文件目录下)。

(3)命令:autoscan。

(4)使用 Vi 编辑 configure.scan 为:


#                                               -*- Autoconf -*-

# Process this file with autoconf to produce a configure script.



AC_PREREQ(2.59)

AC_INIT(hello, 1.0)

AM_INIT_AUTOMAKE(hello,1.0)

AC_CONFIG_SRCDIR([hello.h])

AC_CONFIG_HEADER([config.h])

# Checks for programs.

AC_PROG_CC

# Checks for libraries.

# Checks for header files.

# Checks for typedefs, structures, and compiler characteristics.

# Checks for library functions.

AC_OUTPUT(Makefile)

 

 

(5)保存退出,并重命名为 configure.in。

(6)运行:aclocal。

(7)运行:autoconf,并用 ls 查看是否生成了 configure 可执行文件。

(8)运行:autoheader。

(9)用 Vi 编辑 Makefile.am 文件为:

AUTOMAKE_OPTIONS=foreign

bin_PROGRAMS=hello

hello_SOURCES=hello.c hello.h

(10)运行:automake。

(11)运行:./configure。

(12)运行:make。

(13)运行:./hello,查看结果是否正确。

(14)运行:make install。

(15)运行:hello,查看结果是否正确。

(16)运行:make dist。

(17)在当前目录下解压 hello-1.0.tar.gz:tar –zxvf hello-1.0.tar.gz。

(18)进入解压目录:cd ./hello-1.0。

(19)下面开始 Linux 下常见的安装软件步骤:./configure。

(20)运行:make。

(21)运行:./hello(在正常安装时这一步可省略)。

(22)运行:make install。

(23)运行:hello,查看结果是否正确。

 

4.实验结果

能够正确使用 autotools 生成 Makefile,并且能够安装成功短小的 Hello 软件。

思考与练习

在 Linux 下使用 Gcc 编译器和 Gdb 调试器编写汉诺塔游戏程序。


第5章 嵌入式 Linux 开发环境的搭建

5.1 嵌入式交叉编译环境的搭建

交叉编译的概念在第 4 章中已经详细讲述过,搭建交叉编译环境是嵌入式开发的第一步,也是必备一步。搭建交叉编译环境的方法很多,不同的体系结构、不同的操作内容甚至是不同版本的内核,都会用到不同的交叉编译器,而且,有些交叉编译器经常会有部分的 BUG,这都会导致最后的代码无法正常地运行。因此,选择合适的交叉编译器对于嵌入式开发是非常重要的。交叉编译器完整的安装一般涉及到多个软件的安装(读者可以从 ftp://gcc.gnu.org/pub/下载),包括 binutils、gcc、glibc 等软件。其中,binutils 主要用于生成一些辅助工具,如 objdump、as、ld 等;gcc 是用来生成交叉编译器,主要生成 arm-linux-gcc 交叉编译工具(应该说,生成此工具后已经搭建起了交叉编译环境,可以编译 Linux 内核了,但由于没有提供标准用户函数库,用户程序还无法编译);glibc   主要是提供用户程序所使用的一些基本的函数库。这样,交叉编译环境就完全搭建起来了。

上面所述的搭建交叉编译环境比较复杂,很多步骤都涉及到对硬件平台的选择。因此,

现在提供开发板的公司一般会在附赠的光盘中提供该公司测试通过的交叉编译器,而且很多公司把以上安装步骤全部写入脚本文件或者以发行包的形式提供,这样就大大方便了用户的使用。如优龙的开发光盘里就随带了 2.95.3 和3.3.2两个版本的交叉编译器,其中前一个版本是用于编译 Linux2.4 内核的,而后一个版本是用于编译 Linux2.6 版本内核的。由于这是厂商测试通过的编译器,因此可靠性会比较高,而且与开发板能够很好地吻合。所以推荐初学者直接使用厂商提供的编译器。当然,由于时间滞后的原因,这个编译器往往不是最新版本的,若需要更新时希望读者另外查找相关资料学习。本书就以广嵌自带的 cross-3.3.2 为例进行讲解(具体的名称不同厂商可能会有区别)。

安装交叉编译器的具体步骤在第 2 章的实验二中已经进行了详细地讲解了,在此仅回忆关键步骤,对于细节请读者参见第 2 章的实验二。

在/usr/local/arm 下解压3.3.2.bar.bz2。

  

[root@localhost arm]# tar jxvf cross-3.3.2.bar.bz2

[root@localhost arm]# ls

3.3.2  cross-3.3.2.tar.bz2

[root@localhost arm]# cd ./3.3.2

[root@localhost arm]# ls

arm-linux  bin  etc  include  info  lib  libexec  man  sbin  share  VERSIONS

[root@localhost bin]# which arm-linux*

/usr/local/arm/3.3.2/bin/arm-linux-addr2line

/usr/local/arm/3.3.2/bin/arm-linux-ar

/usr/local/arm/3.3.2/bin/arm-linux-as

 /usr/local/arm/3.3.2/bin/arm-linux-c++

/usr/local/arm/3.3.2/bin/arm-linux-c++filt

/usr/local/arm/3.3.2/bin/arm-linux-cpp

/usr/local/arm/3.3.2/bin/arm-linux-g++

/usr/local/arm/3.3.2/bin/arm-linux-gcc

/usr/local/arm/3.3.2/bin/arm-linux-gcc-3.3.2

/usr/local/arm/3.3.2/bin/arm-linux-gccbug

/usr/local/arm/3.3.2/bin/arm-linux-gcov

/usr/local/arm/3.3.2/bin/arm-linux-ld

/usr/local/arm/3.3.2/bin/arm-linux-nm

/usr/local/arm/3.3.2/bin/arm-linux-objcopy

/usr/local/arm/3.3.2/bin/arm-linux-objdump

/usr/local/arm/3.3.2/bin/arm-linux-ranlib

/usr/local/arm/3.3.2/bin/arm-linux-readelf

/usr/local/arm/3.3.2/bin/arm-linux-size

/usr/local/arm/3.3.2/bin/arm-linux-strings

/usr/local/arm/3.3.2/bin/arm-linux-strip

 

可以看到,在/usr/local/arm/3.3.2/bin/下已经安装了很多交叉编译工具。用户可以查看 arm文件夹下的 VERSION 文件,显示如下:

Versions

gcc-3.3.2

  glibc-2.3.2

  binutils-head

Tool chain binutils configuration:

../binutils-head/configure            



../glibc-2.3.2/configure

Tool chain gcc configuration

../gcc-3.3.2/configure           

 

可以看到,这个广嵌公司提供的交叉编译工具确实集成了 binutils、gcc、glibc 这几个软件,而每个软件也都有比较复杂的配置信息,读者可以查看 Version 文件了解相关信息。

5.2  超级终端和 Minicom 配置及使用

前文已知,嵌入式系统开发的程序运行环境是在硬件开发板上的,那么如何把开发板上的信息显示给开发人员呢?最常用的就是通过串口线输出到宿主机的显示器上,这样,开发人员就可以看到系统的运行情况了。在 Windows 和 Linux 中都有不少串口通信软件,可以很方便地对串口进行配置,其中最主要的配置参数就是波特率、数据位、停止位、奇偶校验位和数据流控制位等,但是它们一定要根据实际情况进行相应配置。下面介绍 Windows 中典型的串口通信软件“超级终端”和在 Linux 下的“Minicom”。

1.超级终端

首先,打开 Windows 下的“开始”→“附件”→“通讯”→“超级终端”,这时会出现

如图 5.1 所示的新建超级终端界面,在“名称”处可随意输入该连接的名称。

接下来,将“连接时使用”的方式改为“COM1”,即通过串口 1,如图 5.2 所示。

接下来就到了最关键的一步——设置串口连接参数。要注意,每块开发板的连接参数有可能会有差异,其中的具体数据在开发商提供的用户手册中会有说明。如优龙的这款 FS2410采用的是波特率:115200,数据为 8 位,无奇偶校验位,停止位 1,无硬件流,其对应配置如图 5.3 所示。

这样,就基本完成了配置,最后一步“单击”确定就可以了。这时,读者可以把开发板的串口线和  机相连,若配置正确,在开发板上电后在超级终端的窗口里应能显示类似如图 5.4 的串口信息。


 图5.1    新建超级终端界面

图    选择连接时使用方式                 图    配置串口相关参数


注意   要分清开发板上的串口 1,串口 2,如在优龙的开发板上标有“UART1”、“UATR2”,否则串口无法打印出信息。

2.Minicom


Minicom 是 Linux 下串口通信的软件,它的使用完全依靠键盘的操作,虽然没有“超级终端”那么易用,但是使用习惯之后读者将会体会到它的高效与便利。下面主要讲解如何对Minicom 进行串口参数的配置。

图    串口相关信息


首先在命令行中键入“minicom”,这就启动了 minicom 软件。Minicom 在启动时默认会进行初始化配置,如图 5.5 所示。

              图    minicom 启动

注意   在 Minicom 的使用中,经常会遇到三个键的操作,如“CTRL-A Z”,这表示先同时按下 CTRL和“A”(大写),然后松开此二键再按下“Z”。

正如图 5.5 中的提示,接下来可键入 CTRL+A Z,来查看 minicom 的帮助,如图 5.6所示。

按照帮助所示,可键入“O”(代表  Minicom)来配置 minicom 的串口参数,当然也可以直接键入“CTRL-A O”来进行配置。如图 5.7 所示。

图    minicom 帮助

            图    minicom 配置界面

在这个配置框中选择“Serial port setup”子项,进入如图 5.8 所示配置界面。

上面列出的配置是 minicom 启动是的默认配置,用户可以通过键入每一项前的大写字母,分别对每一项进行更改。图 5.9 所示为在“Change which setting 中”键入了“A”,此时光标转移到第 A 项的对应处。

注意  在 minicom 中“ttyS0”对应“COM1”,“ttyS1”对应“COM2”。

                    图5.8  minicom 串口属性配置界面



图    minicom 串口号配置

 

接下来,要对波特率、数据位和停止位进行配置,键入“E”,进入如图 5.10 所示的配置界面。

在该配置界面中,可以键入相应波特率、停止位等对应的字母,即可实现配置,配置完成后按回车键就退出了该配置界面,在上层界面中显示如图 5.11 所示配置信息,要注意与图5.8 进行对比,确定相应参数是否已被重新配置。

 

图   minicom 波特率等配置界面

            图    minicom 配置完成后界面

                 图    minicom 保存配置信息

                 图    minicom 显示串口信息

 

 

到此为止,读者已经能将开发板的系统情况通过串口打印到宿主机上了,这样,就能很好地了解硬件的运行状况。

小知识  通过串口打印信息是一个很常见的手段,很多其他情况如路由器等也是通过配置串口的波特率这些参数来显示对应信息的。

5.3  下载映像到开发板

正如第 4 章中所述,嵌入式开发的运行环境是目标板,而开发环境是宿主机。因此,需要把宿主机中经过编译之后的可执行文件下载到目标板上去。要注意的是,这里所说的下载是下载到目标机中的SDRAM。然后,用户可以选择直接从SDRAM 中运行或写入到 Flash中再运行。运行常见的下载方式有网络下载(如 tftp、ftp 等方式)、串口下载、USB 下载等,本书主要讲解网络下载中的 tftp 方式和串口下载方式。

1.tftp

Tftp 协议是简单文件传输协议,它可以看作是一个 FTP 协议的简化版本,与 FTP 协议相比,它的最大区别在于没有用户管理的功能。它的传输速度快,可以通过防火墙,使用方便快捷,因此在嵌入式的文件传输中广泛使用。

同 FTP 一样,tftp 分为客户端和服务器端两种。通常,首先在宿主机上开启 tftp 服务器端服务,设置好 tftp 的根目录内容(也就是供客户端下载的文件),接着,在目标板上开启 tftp的客户端程序(现在很多开发板都已经提供了该项功能)。这样,把目标板和宿主机用直连线相连之后,就可以通过 tftp 协议传输可执行文件了。

下面分别讲述在 Linux 下和 Windows 下的配置方法。

(1)Linux 下 tftp 服务配置

Linux 下 tftp 的服务器服务是由 xinetd 所设定的,默认情况下是处于关闭状态。

首先,要修改 tftp 的配置文件,开启 tftp 服务,如下所示:

[root@ateng tftpboot]# vi /etc/xinetd.d/tftp

# default: off

#

#            protocol.  The tftp protocol is often used to boot diskless \

#            workstations, download configuration files to network-aware printers, \

#            and to start the installation process for some operating systems.

service tftp

{

socket_type                                         = dgram

protocol                     = udp

wait                         = yes

user                         = root

server                                              = /usr/sbin/in.tftpd

server_args                                         = -s /tftpboot

disable                                             = no

per_source                                          = 11

cps                                                 = 100 2

flags                                               = IPv4

}

在这里,主要要将“disable=yes”改为“no”,另外,从“server_args”可以看出,tftp

服务器端的默认根目录为“/tftpboot”,用户若需要可以更改为其他目录。接下来,重启 xinetd 服务,使刚才的更改生效,如下所示:

[root@ateng tftpboot]# service xinetd restart

关闭 xinetd:                                             [    确定  ]

启动 xinetd:                                             [    确定  ]

接着,使用命令“netstat -au”以确认 tftp 服务是否已经开启,如下所示:

[root@ateng tftpboot]# netstat -au

Active Internet connections (servers and established)

Proto Recv-Q Send-Q Local Address               Foreign Address       State

udp        0      0 *:32768                             *:*

udp        0      0 *:831                               *:*

udp        0      0 *:tftp                              *:*

udp        0      0 *:sunrpc                            *:*

udp        0      0 *:ipp                               *:*

 

这时,用户就可以把所需要的传输文件放到“/tftpboot”目录下,这样,主机上的 tftp 服务就可以建立起来了。

接下来,用直连线(注意:不可以使用网线)把目标板和宿主机连起来,并且将其配置成一个网段的地址,再在目标板上启动 tftp 客户端程序(注意:不同的开发板所使用的命令可能会不同,读者可以查看帮助来获得确切的命令名及格式),如下所示:

=>tftpboot 0x30200000 zImage

TFTP from server 192.168.1.1; our IP address is 192.168.1.100

Filename 'zImage'.

Load address: 0x30200000

Loading: #################################################################

           ###############################################################

           #############################################

done

Bytes transferred = 881988 (d7544 hex)

 

可以看到,此处目标板使用的 IP 为“192.168.1.100”,宿主机使用的 IP 为“192.168.1.1”,下载到目标板的地址为 0x30200000,文件名为“zImage”。

(2)Windows 下 tftp 服务配置

在 Windows 下配置为 tftp 服务器端需要下载 tftp 服务器软件,常见的为 tftpd32。

首先,单击 tftpd32 下方的设置按钮,进入设置界面,如图 5.14 所示,在这里,主要配置 tftp 服务器端地址,也就是本机的地址。

接下来,重新启动 tftpd32 软件使刚才的配置生效,这样服务器端的配置就完成了,这时,就可以用直连线连接目标机和宿主机,且在目标机上开启 tftp 服务进行文件传输,这时,tftp服务器端如图 5.15 和图 5.16 所示。

图    tftpd32 配置界面                     图    tftp 文件传输


图    tftp 服务器端显示情况

 



小知识   tftp 是一个很好的文件传输协议,它的简单易用吸引了广大用户。但它同时也存在着较大的安全隐患。由于 tftp 不需要用户的身份认证,因此给了黑客的可乘之机。因此在使用 tftp 时一定要设置一个单独的目录作为 tftp 服务的根目录,如上文所述的“/tftpboot”等。


 2.串口下载

   使用串口下载需要配合特定的下载软件,如优龙公司提供的DNW软件等,一般在Windows 下进行操作。虽然串口下载的速度没有网络下载快,但由于它很方便,不需要额外的连线和设置 IP 等操作,因此也广受用户的青睐。下面就以 DNW 软件为例,介绍串口下载的方式。

  与其他串口通信的软件一样,在 DNW 中也要设置“波特率”、“端口号”等。打开“Configuration”下的“Options”界面,如图 5.17 所示。

              图    DNW 配置界面

 

在配置完之后,单击“Serial Port”下的“Connect”,再将开发板上电,选择“串口下载”,

接着再在“Serial  Port”下选择“Transmit”,这时,就可以进行文件传输了,如图 5.18 和图5.19 所示。这里 DNW 默认串口下载的地址为 0x30200000


图    DNW 串口下载图

                图    DNW 串口下载情形图

5.4  编译嵌入式 Linux 内核

在做完了前期的准备工作之后,在这一步,读者就可以编译嵌入式移植 Linux 的内核了。在这里,本书主要介绍嵌入式   Linux 内核的编译过程,在下一节会进一步介绍嵌入式 Linux中体系结构相关的内核代码,读者在此之后就可以尝试嵌入式 Linux 操作系统的移植。编译嵌入式 Linux 内核都是通过 make 的不同命令来实现的,它的执行配置文件就是在第 3 章中讲述的 Makefile。Linux   内核中不同的目录结构里都有相应的 Makefile,而不同的Makefile 又通过彼此之间的依赖关系构成统一的整体,共同完成建立依存关系、建立内核等功能。

内核的编译根据不同的情况会有不同的步骤,但其中最主要分别为 3 个步骤:内核配置、建立依存关系、建立内核,其他的为一些辅助功能,如清除文件等。读者在实际编译时若出现错误等情况,可以考虑采用其他辅助功能。下面分别讲述这 3 步主要的步骤。

(1)内核配置

第一步内核配置中的选项主要是用户用来为目标板选择处理器架构的选项,不同的处理器架构会有不同的处理器选项,比如 ARM 就有其专用的选项如“Multimedia capabilities portdrivers”等。因此,在此之前,必须确保在根目录中  里“ARCH”的值已设定了目标板的类型,如:

ARCH   := arm


接下来就可以进行内核配置了,内核支持 4 种不同的配置方法,这几种方法只是与用户交互的界面不同,其实现的功能是一样的。每种方法都会通过读入了一个默认的配置文件—根目录下“.config”隐藏文件(用户也可以手动修改该文件,但不推荐使用)。当然,用户也可以自己加载其他配置文件,也可以将当前的配置保存为其他名字的配置文件。

这 4 种方式如下。

    make config:基于文本的最为传统的配置界面,不推荐使用。

    make menuconfig:基于文本选单的配置界面,字符终端下推荐使用。

    make xconfig:基于图形窗口模式的配置界面,Xwindow 下推荐使用。

    make  oldconfig:自动读入“.config”配置文件,并且只要求用户设定前次没有设定过的选项。

在这 4 种模式中,make menuconfig 使用最为广泛,下面就以 make menuconfig 为例进行讲解,如图 5.20 所示。

从该图中可以看出,Linux 内核允许用户对其各类功能逐项配置,一共有 18 类配置选项,这里就不对这   18  类配置选项进行一一讲解了,需要的读者可以参见相关选项的  help。在menuconfig 的配置界面中是纯键盘的操作,用户可使用上下键和“Tab”键移动光标以进入相关子项,图 5.21 所示为进入了“System Type”子项的界面,该子项是一个重要的选项,主要用来选择处理器的类型。

可以看到,每个选项前都有个括号,可以通过按空格键或“Y”键表示包含该选项,按“N”表示不包含该选项。

另外,读者可以注意到,这里的括号有 3 种,即中括号、尖括号或圆括号。读者可以用空格键选择相应的选项时可以发现中括号里要么是空,要么是“*”;尖括号里可以是空,“*”和“M”,分别表示包含选项、不包含选项和编译成模块;圆括号的内容是要求用户在所提供的几个选项中选择一项。

图    make menuconfig 配置界面


    图    System Type 子项

此外,要注意 2.6 和 2.4 内核在串口命名上的一个重要区别,在 2.4 内核中“COM1”对应的是“ttyS0”,而在 2.6 内核中“COM1”对应“ttySAC0”,因此在启动参数的子项要格外注意,如图 5.22 所示,否则串口打印不出信息。

一般情况下,使用厂商提供的默认配置文件都能正常运行,所以用户初次使用时可以不

用对其进行额外的配置,在以后使用需要其他功能时再另行添加,这样可以大大减少出错的几率,有利于错误定位。在完成配置之后,就可以保存退出,如图 5.23 所示。

 

                  图    启动参数配置子项

图    保存退出

(2)建立依赖关系

由于内核源码树中的大多数文件都与一些头文件有依赖关系,因此要顺利建立内核,内核源码树中的每个 Makefile 就必须知道这些依赖关系。建立依赖关系往往发生在第一次编译内核的时候,它会在内核源码树中每个子目录产生一个“.depend”文件。运行“make dep”即可。

(3)建立内核

建立内核可以使用“make zImage”或“make bzImage”,这里建立的为压缩的内核映像。

通常在 Linux 中,内核映像分为压缩的内核映像和未压缩的内核映像。其中,压缩的内核映像通常名为   zImage,位于“arch/$(ARCH)/boot”目录中。而未压缩的内核映像通常名为vmlinux,位于源码树的根目录中。

到这一步就完成了内核源代码的编译,之后,读者可以使用上一小节所讲述的方法把内核压缩文件下载到开发板上运行。

小知识

在嵌入式 Linux 的源码树中通常有以下几个配置文件,“.config”、“autoconf.h”、“config.h”,其中“.config”文件是 make menuconfig 默认的配置文件,位于源码树的根目录中。“autoconf.h” 和“config.h”是以宏的形式表示了内核的配置,当用户使用 make  menuconfig 做了一定的更改之后,系统自动会在“autoconf.h”和“config.h”中做出相应的更改。它们位于源码树的“/include/linux/”下。

 

5.5  Linux 内核目录结构

Linux 内核的目录结构如图 5.24 所示。

    /include   子目录包含了建立内核代码时所需的大部分

包含文件,这个模块利用其他模块重建内核。

    /init   子目录包含了内核的初始化代码,这是内核工作

的开始的起点。

    /arch   子目录包含了所有硬件结构特定的内核代码。

如:arm、i386、alpha。

    /drivers 子目录包含了内核中所有的设备驱动程序,如

块设备和 SCSI 设备。

     /fs    子目录包含了所有的文件系统的代码,如:ext2,

vfat 等。

    /net 子目录包含了内核的连网代码。

    /mm 子目录包含了所有内存管理代码。

    /ipc 子目录包含了进程间通信代码。

    /kernel 子目录包含了主内核代码。

        图    Linux 内核目录结构

5.6  制作文件系统

可以看到,系统启动时发生了加载文件系统的错误。要记住,上一节所编译的仅仅是内核,文件系统和内核是完全独立的两个部分。读者可以回忆一下第 2 章讲解的 Linux 启动过程的分析(嵌入式 Linux 是 Linux 裁减后的版本,其精髓部分是一样的),其中在 head.S 中就加载了根文件系统。因此,加载根文件系统是 Linux 启动中不可缺少的一部分。本节就来讲解嵌入式 Linux 中文件系统和根文件系统的制作方法。

制作文件系统的方法有很多,可以从零开始手工制作,也可以在现有的基础上添加部分内容加载到目标板上去。由于完全手工制作工作量比较大,而且也很容易出错,因此,本节将主要介绍把现有的文件系统加载到目标板上的方法,主要包括制作文件系统镜像和用 NFS加载文件系统的方法。

1.制作文件系统镜像

读者已经知道,Linux 支持多种文件系统,同样,嵌入式   也支持多种文件系统。虽然在嵌入式中,由于资源受限的原因,它的文件系统和 Linux 的文件系统有较大的区别(前者往往是只读文件系统),但是,它们的总体架构是一样的,都是采用目录树的结构。在嵌入式中常见的文件系统有 cramfs、romfs、jffs、yaffs 等,这里就以制作 cramfs 文件系统为例进行讲解。cramfs 文件系统是一种经压缩的、极为简单的只读文件系统,因此非常适合嵌入式系统。要注意的是,不同的文件系统都有相应的制作工具,但是其主要的原理和制作方法是类似的。

制作 cramfs 文件系统需要用到的工具是 mkcramfs,下面就来介绍使用 mkcramfs 制作文件系统映像的方法。这里假设用户已经有了一个 cramfs 文件系统,在目录“/root/workplace/fs/guo”里,如下所示:

[root@localhost guo]# ls

bin  dev  etc  home  lib  linuxrc  proc  Qtopia  ramdisk  sbin  tmp  usr  var

接下来就可以使用 mkcramfs 工具了,格式为:mkcramfs dir name,如下所示。

[root@localhost fs]# ./mkcramfs guo FS2410XP_camare_demo4.cramfs

-21.05% (-64 bytes)         Tongatapu

-21.03% (-49 bytes)        Truk

-21.03% (-49 bytes)        Wake

-22.41% (-52 bytes)        Wallis

-21.95% (-54 bytes)        Yap

-17.19% (-147 bytes)       WET

-47.88% (-8158 bytes)      zone.tab

-55.24% (-17421 bytes)     usb-storage.o

-54.18% (-16376 bytes)     usbvideo.o

-54.07% (-2736 bytes)      videodev.o

Everything: 27628 kilobytes

Super block: 76 bytes

CRC: e3a6d7ca

可以看到,mkcramfs 在制作文件镜像的时候对文件进行了压缩。

读者可以先在本机上通过 mount 进行验证,如下所示:

 

[root@localhost fs]#             mkdir ateng

[root@localhost fs]# mount -o loop FS2410XP_camare_demo4.cramfs ./ateng

[root@localhost fs]# ls ateng

bin  dev  etc  home  lib  linuxrc  proc  Qtopia  ramdisk  sbin  tmp  usr  var

 

接下来,就可以烧入到开发板的相应部分了。

2.NFS 文件系统

NFS 为  FileSystem 的简称,最早是由 Sun 公司提出发展起来的,其目的就是让不同的机器、不同的操作系统之间可以彼此共享文件。NFS 可以让不同的主机通过网络将远端的 NFS 服务器共享出来的文件安装到自己的系统中,从客户端看来,使用 NFS 的远端文件就像是使用本地文件一样。在嵌入式中使用 NFS 会使应用程序的开发变得十分方便,并且不用反复地进行烧写镜像文件。

NFS 的使用分为服务器端和客户端,其中服务器端是提供要共享的文件,而客户端则通过挂载“mount”这一动作来实现对共享文件的访问操作。下面主要介绍 NFS 服务器端的使用。

NFS    服务器端是通过读入它的配置文件“/etc/exports”来决定所共享的文件目录的。下面首先讲解这个配置文件的书写规范。

在这个配置文件中,每一行都代表一项要共享的文件目录以及所指定的客户端对其的操作权限。客户端可以根据相应的权限,对该目录下的所有目录文件进行访问。配置文件中每一行的格式如下:

[共享的目录] [主机名称或 IP] [参数 1,参数 2…]

在这里,主机名或 IP 是可供共享的客户端主机名或 IP,若对所有的 IP 都可以访问,则可用“*”表示。

这里的参数有很多中组合方式,常见的参数如表 5.1 所示。

表  常见参数

如在本例中,配置文件“/etc/exports”的代码如下:

[root@localhost fs]# cat /etc/exports

/root/workplace    *(rw,no_root_squash)

在设定完配置文件之后,需要启动 nfs 服务和 portmap 服务,这里的 portmap 服务是允许NFS 客户端查看 NFS 服务在用的端口,在它被激活之后,就会出现一个端口号为 111 的  RPC(远端过程调用)的服务。这是 NFS 服务中必须实现的一项,因此,也必须把它开启。

如下所示:

[root@localhost fs]#  service portmap start

启动                  [确定]

[root@localhost fs]# service nfs start

启动服务:      [确定]

关掉配额:     [确定]

启动守护进程:     [确定]

启动 NFS mountd:    [确定]

 

可以看到,在启动 NFS 服务的时候启动了 mountd 进程。这是 NFS 挂载服务,用于处理NFSD 递交过来的客户端请求。另外还会激活至少两个以上的系统守护进程,然后就开始监听客户端的请求,用 cat/var/log/messages 可以看到操作是否成功。这样,就启动了 NFS 的服务,另外还有两个命令,可以方便 NFS 的使用。

其一是 exportfs,它可以重新扫描“/etc/exports”,使用户在修改了“/etc/exports”配置文件不需要每次重启 NFS 服务。其格式为:

exportfs [选项] 

exportfs 的常见选项如表 5.2 所示。

表    常见选项

另外一个是 showmount 命令,它用于当前的挂载情况。其格式为:

showmount [选项] hostname


showmount 的常见选项如表 5.3 所示。


表    常见选项

思考与练习

1.适当更改 Linux 内核配置,再进行编译下载查看结果。

2.配置 NFS 服务。


第6章 文件 I/O 编程

6.1  Linux 系统调用及用户编程接口(API)

由于本章是讲解 Linux 编程开发的第 1 章,因此希望读者更加明确 Linux 系统调用和用户编程接口(API)的概念。在了解了这些之后,会对 Linux 以及 Linux 的应用编程有更深入地理解。

6.1.1  系统调用

所谓系统调用是指操作系统提供给用户程序调用的一组“特殊”接口,用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务。例如用户可以通过进程控制相关的系统调用来创建进程、实现进程调度、进程管理等。

在这里,为什么用户程序不能直接访问系统内核提供的服务呢?这是由于在 Linux 中,为了更好地保护内核空间,将程序的运行空间分为内核空间和用户空间(也就是常称的内核态和用户态),它们分别运行在不同的级别上,在逻辑上是相互隔离的。因此,用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间的函数。

但是,在有些情况下,用户空间的进程需要获得一定的系统服务(调用内核空间程序),这时操作系统就必须利用系统提供给用户的“特殊接口”——系统调用规定用户进程进入内核空间的具体位置。进行系统调用时,程序运行空间需要从用户空间进入内核空间,处理完后再返回到用户空间。

Linux 系统调用部分是非常精简的系统调用(只有 250 个左右),它继承了 UNIX 系统调用中最基本和最有用的部分。这些系统调用按照功能逻辑大致可分为进程控制、进程间通信、文件系统控制、系统控制、存储管理、网络管理、socket 控制、用户管理等几类。

6.1.2  用户编程接口(API)

前面讲到的系统调用并不是直接与程序员进行交互的,它仅仅是一个通过软中断机制向内核提交请求,以获取内核服务的接口。在实际使用中程序员调用的通常是用户编程接口——API,也就是本书后面要讲到的    API 函数。但并不是所有的函数都一一对应一个系统调用,有时,一个 API 函数会需要几个系统调用来共同完成函数的功能,甚至还有一些 API 函数不需要调用相应的系统调用(因此它所完成的不是内核提供的服务)。

在 Linux 中,用户编程接口(API)遵循了在 UNIX 中最流行的应用编程界面标准——POSIX标准。POSIX 标准是由 IEEE 和 ISO/IEC 共同开发的标准系统。该标准基于当时现有的 UNIX 实践和经验,描述了操作系统的系统调用编程接口(实际上就是 API),用于保证应用程序可以在源代码一级上在多种操作系统上移植运行。这些系统调用编程接口主要是通过 C 库(libc)实现的。

6.1.3  系统命令

以上讲解了系统调用、用户编程接口(API)的概念,分析了它们之间的相互关系,那么,读者在第 2 章中学到的那么多的 Shell 系统命令与它们之间又是怎样的关系呢?

系统命令相对 API 更高了一层,它实际上是一个可执行程序,它的内部引用了用户编程接口(API)来实现相应的功能。它们之间的关系如下图 6.1 所示。

6.2  Linux 中文件及文件描述符概述

正如第 1 章中所述,在 Linux 中对目录和设备的操作都等同于文件的操作,因此,大大简化了系统对不同设备的处理,提高了效率。Linux   中的文件主要分为 4 种:普通文件、目录文件、链接文件和设备文件。

那么,内核如何区分和引用特定的文件呢?这里用到的就是一个重要的概念——文件描述符。对于 Linux 而言,所有对设备和文件的操作都使用文件描述符来进行的。文件描述符是一个非负的整数,它是一个索引值,并指向内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。

 

通常,一个进程启动时,都会打开 3 个文件:标准输入、标准输出和标准出错处理。这3 个文件分别对应文件描述符为 0、1 和 2(也就是宏替换 STDIN_FILENO、STDOUT_FILENO和 STDERR_FILENO,鼓励读者使用这些宏替换)。

基于文件描述符的 I/O 操作虽然不能移植到类 Linux 以外的系统上去(如 Windows),但它往往是实现某些 I/O 操作的惟一途径,如 Linux 中低级文件操作函数、多路 I/O、TCP/IP 套接字编程接口等。同时,它们也很好地兼容 POSIX 标准,因此,可以很方便地移植到任何 POSIX 平台上。基于文件描述符的 I/O 操作是 Linux 中最常用的操作之一,希望读者能够很好地掌握

6.3  不带缓存的文件 I/O 操作

本节主要介绍不带缓存的文件 I/O 操作,主要用到 5 个函数:open、read、write、lseek和close。这里的不带缓存是指每一个函数都只调用系统中的一个函数。这些函数虽然不是ANSI C 的组成部分,但是是POSIX的组成部分。

6.3.1  open 和

(1)open 和 close 函数说明

open 函数是用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。

close 函数是用于关闭一个打开文件。当一个进程终止时,它所有已打开的文件都由内核自动关闭,很多程序都使用这一功能而不显示地关闭一个文件。

(2)open 和 close 函数格式

open 函数的语法格式如表 6.1 所示。

   

  在 open 函数中,flag 参数可通过“|”组合构成,但前 3 个函数不能相互组合。perms 是文件的存取权限,采用 8 进制表示法,相关内容读者可参见第 2 章。

close 函数的语法格式如下表 6.2 所示。

    表        close 函数语法要点

 

 (3)open 和 close 函数使用实例

下面实例中的 open 函数带有 3 个 flag 参数:O_CREAT、O_TRUNC 和 O_WRONLY,这样就可以对不同的情况指定相应的处理方法。另外,这里对该文件的权限设置为 0600。其源码如下所示:

 

 

/*open.c*/

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdlib.h>

#include <stdio.h>



int main(void)

{

     int fd;

/*调用open 函数,以可读写的方式打开,注意选项可以用|符号连接*/

  if((fd = open("/tmp/hello.c", O_CREAT | O_TRUNC | O_WRONLY , 0600 ))<0)

{

         perror("open:");

         exit(1);

      }

  Else

{

         printf("Open file: hello.c %d\n",fd);

   }

      if( close(fd) < 0 ){

 perror("close:");

 exit(1);

      }

      else

 printf("Close hello.c\n");

      exit(0);

}

[root@(none) 1]# ./open

Open file: hello.c 3

Close hello.c

[root@(none) tmp]# ls -l |grep hello.c

-rw-------    1 root     root            0 Dec  4 00:59 hello.c

 

经过交叉编译后,将文件下载到目标板,则该可执行文件运行后就能在目录/tmp 下新建一个 hello.c 的文件,其权限为 0600。

注意

 open 函数返回的文件描述符一定是最小的未用文件描述符。由于一个进程在启动时自动打开了0、1、2 三个文件描述符,因此,该文件运行结果中返回的文件描述符为 3。读者可以尝试在调用 open 函数之前,加一句 close(0),则此后在 open 函数时返回的文件描述符为 0(若关闭文件描述符 1,则在执行时会由于没有标准输出文件而无法输出)。

6.3.2    read、write 和

(1)read、write 和 lseek 函数作用

read 函数是用于从指定的文件描述符中读出数据放到缓冲区中。当从终端设备文件中读出数据时,通常一次最多读一行。

write 函数是用于向打开的文件写数据,写操作从文件的当前位移量处开始。若磁盘已满或超出该文件的长度,则 write 函数返回失败。

lseek 函数是用于在指定的文件描述符中将文件指针定位到相应的位置。

(2)read 和 write 函数格式

read 函数的语法格式如下表 6.3 所示。

 

在读普通文件时,若读到要求的字节数之前已到达文件的尾部,则返回的字节数会小于希望读出的字节数。

write 函数的语法格式如下表 6.4 所示。

 

在写普通文件时,写操作从文件的当前位移处开始。

lseek 函数的语法格式如下表 6.5 所示。


 

(3)函数使用实例

该示例程序首先打开上一节中创建的文件,然后对此文件进行读写操作(记得要将文件打开属性改为可读写,将文件权限也做相应更改)。接着,写入“Hello! I'm writing to this file!”,此时文件指针位于文件尾部。接着在使用 lseek 函数将文件指针移到文件开始处,并读出 10个字节并将其打印出来。程序源代码如下所示:

/*write.c*/

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#define MAXSIZE

int main(void)

{

     int i,fd,size,len;

     char *buf="Hello! I'm writing to this file!";

     char buf_r[10];

     len = strlen(buf);

/*首先调用 open 函数,并指定相应的权限*/

     if((fd = open("/tmp/hello.c", O_CREAT | O_TRUNC | O_RDWR,0666 ))<0){

perror("open:");

exit(1);

     }

     else

printf("open file:hello.c %d\n",fd);

/*调用 write 函数,将 buf 中的内容写入到打开的文件中*/

if((size = write( fd, buf, len)) < 0){

perror("write:");

exit(1);

}

     else

printf("Write:%s\n",buf);

/*调用 lseek 函数将文件指针移到文件起始,并读出文件中的 10 个字节*/

lseek( fd, 0, SEEK_SET );

if((size = read( fd, buf_r, 10))<0){

perror("read:");

exit(1);

}

     Else

/*buf_r[10]=’\0’其作用是什么*/

printf("read from file:%s\n",buf_r);

     if( close(fd) < 0 ){

perror("close:");

exit(1);

     }

     else

printf("Close hello.c\n");

     exit(0);

}

[root@(none) 1]# ./write

open file:hello.c 3

Write:Hello! I'm writing to this file!

read form file:Hello! I'm(结果出现了些其他看不懂的字符,怎么解决?)

Close hello.c

[root@(none) 1]# cat /tmp/hello.c

Hello! I'm writing to this file!


6.3.3  fcntl

(1)fcntl 函数说明

前面的这 5 个基本函数实现了文件的打开、读写等基本操作,这一节将讨论的是,在文件已经共享的情况下如何操作,也就是当多个用户共同使用、操作一个文件的情况,这时,Linux 通常采用的方法是给文件上锁,来避免共享的资源产生竞争的状态。

文件锁包括建议性锁和强制性锁。建议性锁要求每个上锁文件的进程都要检查是否有锁存在,并且尊重已有的锁。在一般情况下,内核和系统都不使用建议性锁。强制性锁是由内核执行的锁,当一个文件被上锁进行写入操作的时候,内核将阻止其他任何文件对其进行读写操作。采用强制性锁对性能的影响很大,每次读写操作都必须检查是否有锁存在。

在 Linux 中,实现文件上锁的函数有 flock 和 fcntl,其中 flock 用于对文件施加建议性锁,而 fcntl 不仅可以施加建议性锁,还可以施加强制锁。同时,fcntl 还能对文件的某一记录进行上锁,也就是记录锁。

记录锁又可分为读取锁和写入锁,其中读取锁又称为共享锁,它能够使多个进程都能在文件的同一部分建立读取锁。而写入锁又称为排斥锁,在任何时刻只能有一个进程在文件的某个部分上建立写入锁。当然,在文件的同一部分不能同时建立读取锁和写入锁。

注意  fcntl 是一个非常通用的函数,它还可以改变文件进程各方面的属性,在本节中,主要介绍它建立记录锁的方法,关于它其他用户感兴趣的读者可以参看 fcntl 手册。

(2)fcntl 函数格式

用于建立记录锁的 fcntl 函数格式如表 6.6 所示。

 

这里,lock 的结构如下所示:

 

Struct flock{

short l_type;

off_t l_start;

short l_whence;

off_t l_len;

pid_t l_pid;

}

 

lock 结构中每个变量的取值含义如表 6.7 所示。

 


小技巧  为加锁整个文件,通常的方法是将 l_start 说明为 0,l_whence 说明为 SEEK_SET,l_len 说明为 0。

(3)fcntl 使用实例

下面首先给出了使用 fcntl 函数的文件记录锁函数。在该函数中,首先给 flock 结构体的对应位赋予相应的值。接着使用两次 fcntl 函数分别用于给相关文件上锁和判断文件是否可以上锁,这里用到的 cmd 值分别为 F_SETLK 和F_GETLK。

这个函数的源代码如下所示:

 

/*lock_set 函数*/

void lock_set(int fd, int type)

{  

    struct flock lock;


    lock.l_whence = SEEK_SET;//赋值 lock 结构体

    lock.l_start = 0;

    lock.l_len =0;


    while(1)

    {

        lock.l_type = type;

        /*根据不同的 type 值给文件上锁或解锁*/

        if((fcntl(fd, F_SETLK, &lock)) == 0)

        {

            if( lock.l_type == F_RDLCK )

                printf("read lock set by %d\n",getpid());

            else if( lock.l_type == F_WRLCK )

                printf("write lock set by %d\n",getpid());

            else if( lock.l_type == F_UNLCK )

                printf("release lock by %d\n",getpid());

            return;

        }

        /*判断文件是否可以上锁*/

        fcntl(fd, F_GETLK,&lock);

        /*判断文件不能上锁的原因*/

        if(lock.l_type != F_UNLCK)

        {

            /*/该文件已有写入锁*/

            if( lock.l_type == F_RDLCK )

                printf("read lock already set by %d\n",lock.l_pid);

            /*该文件已有读取锁*/

            else if( lock.l_type == F_WRLCK )

                printf("write lock already set by %d\n",lock.l_pid);

            getchar();

        }

    }

}

下面的实例是测试文件的写入锁,这里首先创建了一个 hello 文件,之后对其上写入锁,最后释放写入锁。代码如下所示:

 

/*fcntl_write.c 测试文件写入锁主函数部分*/

#include <unistd.h>

#include <sys/file.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <stdio.h>

#include <stdlib.h>



int main(void)

{


    int fd;

    /*首先打开文件*/

    fd=open("hello",O_RDWR | O_CREAT, 0666);

    if(fd < 0)

    {

        perror("open");

        exit(1);

    }

    /*给文件上写入锁*/

    lock_set(fd, F_WRLCK);

    getchar();

    /*给文件解锁*/

    lock_set(fd, F_UNLCK);

    getchar();

    close(fd);

    exit(0);

}

 

为了能够使用多个终端,更好地显示写入锁的作用,本实例主要在 PC 机上测试,读者可将其交叉编译,下载到目标板上运行。下面是在 PC 机上的运行结果。为了使程序有较大的灵活性,笔者采用文件上锁后由用户键入一任意键使程序继续运行。建议读者开启两个终端,并且在两个终端上同时运行该程序,以达到多个进程操作一个文件的效果。在这里,笔者首先运行终端一,请读者注意终端二中的第一句。

终端一:

[root@localhost file]# ./fcntl_write

write lock set by 4994

release lock by 4994

终端二:

[root@localhost file]# ./fcntl_write

write lock already set by 4994

write lock set by 4997

release lock by 4997

由此可见,写入锁为互斥锁,一个时刻只能有一个写入锁存在。

接下来的程序是测试文件的读取锁,原理同上面的程序一样。

 

/*fcntl_read.c 测试文件读取锁主函数部分*/

#include <unistd.h>

#include <sys/file.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <stdio.h>

#include <stdlib.h>

int main(void)

{

     int fd;

     fd=open("hello",O_RDWR | O_CREAT, 0666);

     if(fd < 0){

    perror("open");

    exit(1);

     }

/*给文件上读取锁*/

lock_set(fd, F_RDLCK);

     getchar();

/*给文件接锁*/

lock_set(fd, F_UNLCK);

     getchar();

     close(fd);

     exit(0);

}

同样开启两个终端,并首先启动终端一上的程序,其运行结果如下所示:

终端一:

[root@localhost file]# ./fcntl2

read lock set by 5009

release lock by 5009


终端二:

[root@localhost file]# ./fcntl2

read lock set by 5010

release lock by 5010

读者可以将此结果与写入锁的运行结果相比较,可以看出,读取锁为共享锁,当进程 5009已设定读取锁后,进程 5010 还可以设置读取锁。

思考   如果在一个终端上运行设置读取锁,则在另一个终端上运行设置写入锁,会有什么结果呢?

6.3.4    select

(1)select 函数说明

前面的 fcntl 函数解决了文件的共享问题,接下来该处理 I/O 复用的情况了。

总的来说,I/O 处理的模型有 5 种。

 阻塞 I/O 模型在这种模型下,若所调用的 I/O 函数没有完成相关的功能就会使进程挂起,直到相关数据到才会出错返回。如常见对管道设备、终端设备和网络设备进行读写时经常会出现这种情况。

 非阻塞模型:在这种模型下,当请求的 I/O 操作不能完成时,则不让进程睡眠,而是返回一个错误。非阻塞 I/O 使用户可以调用不会永远阻塞的 I/O 操作,如 open、write和 read。如果该操作不能完成,则会立即出错返回,且表示该 I/O 如果该操作继续执行就会阻塞。

I/O 多路转接模型:在这种模型下,如果请求的 I/O 操作阻塞,且它不是真正阻塞 I/O,而是让其中的一个函数等待,在这期间,I/O 还能进行其他操作。如本节要介绍的 select 函数和 poll 函数,就是属于这种模型。

 信号驱动 I/O 模型:在这种模型下,通过安装一个信号处理程序,系统可以自动捕获特定信号的到来,从而启动I/O。这是由内核通知用户何时可以启动一个  I/O 操作决定的。

 异步 I/O 模型:在这种模型下,当一个描述符已准备好,可以启动 I/O 时,进程会通知内核。现在,并不是所有的系统都支持这种模型。

可以看到,select 的 I/O 多路转接模型是处理 I/O 复用的一个高效的方法。它可以具体设置每一个所关心的文件描述符的条件、希望等待的时间等,从 select 函数返回时,内核会通知用户已准备好的文件描述符的数量、已准备好的条件等。通过使用 select返回值,就可以调用相应的 I/O 处理函数了。

(2)select 函数格式

Select 函数的语法格式如表 6.8 所示。

 

思考   请读者考虑一下如何确定最高的文件描述符?

可以看到,select函数根据希望进行的文件操作对文件描述符进行了分类处理,这里,对文件描述符的处理主要涉及到 4 个宏函数,如表 6.9 所示。

 


一般来说,在使用 select 函数之前,首先使用 FD_ZERO 和 FD_SET 来初始化文件描述符集,在使用了 select 函数时,可循环使用 FD_ISSET 测试描述符集,在执行完对相关后文件描述符后,使用 FD_CLR 来清除描述符。

另外,select 函数中的 timeout 是一个 struct timeval 类型的指针,该结构体如下所示:

 

struct timeval {

       long tv_sec; /* second */

       long tv_unsec; /* and microseconds*/

}

 

可以看到,这个时间结构体的精确度可以设置到微秒级,这对于大多数的应用而言已经足够了。

(3)使用实例

由于 Select 函数多用于 I/O 操作可能会阻塞的情况下,而对于可能会有阻塞 I/O 的管道、网络编程,本书到现在为止还没有涉及。因此,本例主要表现了如何使用 select 函数,而其中的 I/O 操作是不会阻塞的。

本实例中主要实现将文件 hello1 里的内容读出,并将此内容每隔 10s 写入 hello2 中去。

在这里建立了两个描述符集,其中一个描述符集 inset1 是用于读取文件内容,另一个描述符集 inset2 是用于写入文件的。两个文件描述符 fds[0]和 fds[1]分别指向这一文件描述符。在首先初始化完各文件描述符集之后,就开始了循环测试这两个文件描述符是否可读写,由于在这里没有阻塞,所以文件描述符处于准备就绪的状态。这时,就分别对文件描述符fds[0]和fds[1]进行读写操作。该程序的流程图如图 6.2 所示。

 

/*select.c*/

#include <fcntl.h>

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <time.h>


int main(void)

{

    int fds[2];

    char buf[7];

    int i,rc,maxfd;

    fd_set inset1,inset2;

    struct timeval tv;


    /*首先按一定的权限打开 hello1 文件*/

    if((fds[0] = open ("hello1", O_RDWR|O_CREAT,0666))<0)

        perror("open hello1");


    /*再按一定的权限打开 hello2 文件*/

    if((fds[1] = open ("hello2", O_RDWR|O_CREAT,0666))<0)

        perror("open hello2");


    if((rc = write(fds[0],"Hello!\n",7)))

        printf("rc=%d\n",rc);


    lseek(fds[0],0,SEEK_SET);


    /*取出两个文件描述符中的较大者*/

    maxfd = fds[0]>fds[1] ? fds[0] : fds[1];


    /*初始化读集合 inset1并在读集合中加入相应的描述符*/

    FD_ZERO(&inset1);

    FD_SET(fds[0],&inset1);


    /*初始化写集合 inset2并在写集合中加入相应的描述符*/

    FD_ZERO(&inset2);

    FD_SET(fds[1],&inset2);

    tv.tv_sec=2;

    tv.tv_usec=0;


    /*循环测试该文件描述符是否准备就绪并调用 select 函数对相关文件描述符做对应操作*/

    while(FD_ISSET(fds[0],&inset1)||FD_ISSET(fds[1],&inset2))

    {

        if(select(maxfd+1,&inset1,&inset2,NULL,&tv)<0)

            perror("select");

        else

        {

            if(FD_ISSET(fds[0],&inset1))

            {

                rc = read(fds[0],buf,7);

                if(rc>0)

                {

                    buf[rc]='\0';

                    printf("read: %s\n",buf);

                }

                else

                    perror("read");

            }


            if(FD_ISSET(fds[1],&inset2))

            {

                rc = write(fds[1],buf,7);

                if(rc>0)

                {

                    buf[rc]='\0';

                    printf("rc=%d,write: %s\n",rc,buf);

                }

                else

                    perror("write");


                sleep(10);

            }

        }

    }  

exit(0);

}

 

读者可以将以上程序交叉编译,并下载到开发板上运行。以下是运行结果:

 

[root@(none) 1]# ./select

rc=7

read: Hello!                           /*已到文件末,如何处理可以出现若干次?*/


rc=7,write: Hello!



rc=7,write:Hello!



rc=7,write:Hello!



[root@(none) 1]# cat hello1

Hello!

[root@(none) 1]# cat hello2

Hello!

Hello!

可以看到,使用 select 可以很好地实现 I/O 多路复用,在有阻塞的情况下更能够显示出它的作用。

 

6.4  嵌入式 Linux 串口应用开发

6.4.1    串口概述

用户常见的数据通信的基本方式可分为并行通信与串行通信两种。

 并行通信是指利用多条数据传输线将一个资料的各位同时传送。它的特点是传输速度快,适用于短距离通信,但要求传输速度较高的应用场合。

 串行通信是指利用一条传输线将资料一位位地顺序传送。特点是通信线路简单,利用简单的线缆就可实现通信,降低成本,适用于远距离通信,但传输速度慢的应用场合。

串口是计算机一种常用的接口,常用的串口有 RS-232-C 接口。它是于 1970 年由美国电子工业协会(EIA)联合贝尔系统、调制解调器厂家及计算机终端生产厂家共同制定的用于串行通讯的标准,它的全称是“数据终端设备(DTE)和数据通讯设备(DCE)之间串行二进制数据交换接口技术标准”。该标准规定采用一个 DB25 芯引脚的连接器或 9 芯引脚的连接器,其中 25 芯引脚的连接器如图 6.3 所示。

S3C2440 内部具有 3 个独立的 UART 控制器,每个控制器都可以工作在 Interrupt(中断)模式或者 DMA(直接内存访问)模式。同时,每个 UART 均具有 64 字节的 FIFO(先入先出寄存器),支持的最高波特率可达到 230.4Kbps。UART 的操作主要可分为以下几个部分:资料发送、资料接收、产生中断、产生波特率、Loopback 模式、红外模式以及自动流控模式。

串口参数的配置读者在配置超级终端和 minicom 时也已经接触到过,一般包括波特率、起始位数量、数据位数量、停止位数量和流控协议。在此,可以将其配置为波特率 115200、起始位 1b、数据位 8b、停止位 1b 和无流控协议。

在 Linux  中,所有的设备文件一般都位于“/dev”下,其中串口一、串口二对应的设备名依次为“/dev/ttyS0”、“/dev/ttyS1”,可以查看在“/dev”下的文件以确认。在本章中已经提到过,在 Linux 下对设备的操作方法与对文件的操作方法是一样的,因此,对串口的读写就可以使用简单的“read”,“write”函数来完成,所不同的是只是需要对串口的其他参数另做配置,下面就来详细讲解串口应用开发的步骤。

  图    25 引脚串行接口图

6.4.2    串口设置详解

本节主要讲解设置串口的主要方法。

如前所述,设置串口中最基本的包括波特率设置,校验位和停止位设置。串口的设置主要是设置 struct termios 结构体的各成员值,如下所示:

 

include<termios.h>

struct termio

{    

unsigned short  c_iflag; /* 输入模式标志

  unsigned short  c_oflag;  /*  输出模式标志

unsigned short  c_cflag;  /*  控制模式标志*/

unsigned short  c_lflag;                      /*本地模式标志

unsigned char  c_line;


/* line discipline */

unsigned char  c_cc[NCC];     /* control characters */

};

 

 

在这个结构中最为重要的是 c_cflag,通过对它的赋值,用户可以设置波特率、字符大小、数据位、停止位、奇偶校验位和硬件流控等。另外 c_iflag 和 c_cc 也是比较常用的标志。在此主要对这 3 个成员进行详细说明。

c_cflag 支持的常量名称如表 6.10 所示。其中设置波特率为相应的波特率前加上‘B’,由于数值较多,本表没有全部列出。

 

在这里,对于 c_cflag 成员不能直接对其初始化,而要将其通过“与”、“或”操作使用其中的某些选项。

输入模式  c_iflag    成员控制端口接收端的字符输入处理。c_iflag 支持的变量名称,如表6.11 所示。

c_cc   包含了超时参数和控制字符的定义。c_cc   所支持的常用变量名称,如表  6.12所示。

 

下面就详细讲解设置串口属性的基本流程。

1.保存原先串口配置

首先,为了安全起见和以后调试程序方便,可以先保存原先串口的配置,在这里可以使用函数 tcgetattr(fd,&oldtio)。该函数得到与 fd 指向对象的相关参数,并将它们保存于 oldtio引用的 termios 结构中。该函数还可以测试配置是否正确、该串口是否可用等。若调用成功,函数返回值为 0,若调用失败,函数返回值为−1,其使用如下所示:

 

if  ( tcgetattr( fd,&oldtio)  !=  0) {

      perror("SetupSerial 1");

      return -1;  

}

 

2.激活选项有 CLOCAL 和

CLOCAL 和 CREAD 分别用于本地连接和接受使能,因此,首先要通过位掩码的方式激活这两个选项。

newtio.c_cflag  |=  CLOCAL | CREAD;

3.设置波特率

设置波特率有专门的函数,用户不能直接通过位掩码来操作。设置波特率的主要函数有:cfsetispeed 和 cfsetospeed。这两个函数的使用很简单,如下所示:

cfsetispeed(&newtio, B115200);

cfsetospeed(&newtio, B115200);

一般地,用户需将输入输出函数的波特率设置成一样的。这几个函数在成功时返回0,失败时返回1。

4.设置字符大小

与设置波特率不同,设置字符大小并没有现成可用的函数,需要用位掩码。一般首先去除数据位中的位掩码,再重新按要求设置。如下所示:

options.c_cflag &=~CSIZE;/* mask the character size bits */

options.c_cflag |= CS8;

 

5.设置奇偶校验位

设置奇偶校验位需要用到两个 termio 中的成员:c_cflag 和 c_iflag。首先要激活 c_cflag中的校验位使能标志 PARENB 和是否要进行偶校验,同时还要激活 c_iflag 中的奇偶校验使能。如使能奇校验时,代码如下所示:

newtio.c_cflag |= PARENB;

newtio.c_cflag |= PARODD;

newtio.c_iflag |= (INPCK | ISTRIP);


而使能偶校验时代码为:


newtio.c_iflag |= (INPCK | ISTRIP);

newtio.c_cflag |= PARENB;

newtio.c_cflag &= ~PARODD;

6.设置停止位


设置停止位是通过激活 c_cflag 中的 CSTOPB 而实现的。若停止位为 1,则清除 CSTOPB,

若停止位为 0,则激活 CSTOPB。下面是停止位是 1 时的代码:

newtio.c_cflag &=  ~CSTOPB;


7.设置最少字符和等待时间


在对接收字符和等待时间没有特别要求的情况下,可以将其设置为 0,如下所示:

newtio.c_cc[VTIME]  = 0;

newtio.c_cc[VMIN] = 0;


8.处理要写入的引用对象


由于串口在重新设置之后,在此之前要写入的引用对象要重新处理,这时就可调用函数tcflush(fd,queue_selector)来处理要写入引用的对象。对于尚未传输的数据,或者收到的但是尚未读取的数据,其处理方法取决于queue_selector 的值。这里,queue_selector 可能的取值有以下几种。

    TCIFLUSH:刷新收到的数据但是不读。

    TCOFLUSH:刷新写入的数据但是不传送。

    TCIOFLUSH:同时刷新收到的数据但是不读,并且刷新写入的数据但是不传送。

如在本例中所采用的是第一种方法:

tcflush(fd,TCIFLUSH);


9.激活配置

在完成全部串口配置之后,要激活刚才的配置并使配置生效。这里用到的函数是 tcsetattr,

它的函数原型是:

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("SetupSerial 1");

        return -1;

    }


    bzero( &newtio, sizeof( newtio ) );


    /*步骤一,设置字符大小*/

    newtio.c_cflag  |=  CLOCAL | CREAD;

    newtio.c_cflag &= ~CSIZE;


    switch( nBits )

    {

    case 7:

        newtio.c_cflag |= CS7;

        break;

    case 8:

        newtio.c_cflag |= CS8;

        break;

    }

    /*设置奇偶校验位*/

    switch( nEvent )

    {

    case 'O': //奇数

        newtio.c_cflag |= PARENB;

        newtio.c_cflag |= PARODD;

        newtio.c_iflag |= (INPCK | ISTRIP);

        break;

    case 'E': //偶数

        newtio.c_iflag |= (INPCK | ISTRIP);

        newtio.c_cflag |= PARENB;

        newtio.c_cflag &= ~PARODD;

        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:

        cfsetispeed(&newtio, B9600);

        cfsetospeed(&newtio, B9600);

        break;

    case 115200:

        cfsetispeed(&newtio, B115200);

        cfsetospeed(&newtio, B115200);

        break;

    case 460800:

        cfsetispeed(&newtio, B460800);

        cfsetospeed(&newtio, B460800);

        break;

    default:

        cfsetispeed(&newtio, B9600);

        cfsetospeed(&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;

    }

    printf("set done!\n");

    return 0;

}

6.4.3    串口使用详解

在配置完串口的相关属性后,就可以对串口进行打开、读写操作了。它所使用的函数和普通文件读写的函数一样,都是 open、write 和 read。它们相区别的只是串口是一个终端设备,因此在函数的具体参数的选择时会有一些区别。另外,这里会用到一些附加的函数,用于测试终端设备的连接情况等。下面将对其进行具体讲解。


1.打开串口

打开串口和打开普通文件一样,使用的函数同打开普通文件一样,都是 open 函数。如下所示:

fd = open( "/dev/ttyS0", O_RDWR|O_NOCTTY|O_NDELAY);


可以看到,这里除了普通的读写参数外,还有两个参数 O_NOCTTY 和 O_NDELAY。O_NOCTTY 标志用于通知 Linux 系统,这个程序不会成为对应这个端口的控制终端。如果没有指定这个标志,那么任何一个输入(诸如键盘中止信号等)都将会影响用户的进程。O_NDELAY 标志通知 Linux 系统,这个程序不关心 DCD 信号线所处的状态(端口的另一端是否激活或者停止)。如果用户指定了这个标志,则进程将会一直处在睡眠状态,直到DCD 信号线被激活。

接下来可恢复串口的状态为阻塞状态,用于等待串口数据的读入。可用 fcntl 函数实现,如下所示:

fcntl(fd, F_SETFL, 0);

再接着可以测试打开文件描述符是否引用一个终端设备,以进一步确认串口是否正确打开,如下所示:

isatty(STDIN_FILENO);

该函数调用成功则返回 0,若失败则返回-1。

这时,一个串口就已经成功打开了。接下来就可以对这个串口进行读、写操作。

下面给出了一个完整的打开串口的函数,同样写考虑到了各种不同的情况。程序如下所示:

/*打开串口函数*/

int open_port(int fd,int comport)

{

    if (comport==1)//串口

    {

        fd = open( "/dev/ttyS0", O_RDWR|O_NOCTTY|O_NDELAY);

        if (-1 == fd)

        {

            perror("Can't Open Serial Port");

            return(-1);

        }

    }

    else if(comport==2)//串口

    {

        fd = open( "/dev/ttyS1", O_RDWR|O_NOCTTY|O_NDELAY);

        if (-1 == fd)

        {

            perror("Can't Open Serial Port");

            return(-1);

        }

    }

    else if (comport==3)//串口

    {

        fd = open( "/dev/ttyS2", O_RDWR|O_NOCTTY|O_NDELAY);

        if (-1 == fd)

        {

            perror("Can't Open Serial Port");

            return(-1);

        }

    }

    /*恢复串口为阻塞状态*/

    if(fcntl(fd, F_SETFL, 0)<0)

        printf("fcntl failed!\n");

    else

        printf("fcntl=%d\n",fcntl(fd, F_SETFL,0));


    /*测试是否为终端设备*/

    if(isatty(STDIN_FILENO)==0)

        printf("standard input is not a terminal device\n");

    else

        printf("isatty success!\n");


    printf("fd-open=%d\n",fd);


    return fd;

}

2.读写串口

读写串口操作和读写普通文件一样,使用 read、write 函数即可。如下所示:

write(fd,buff,8);

read(fd,buff,8);


下面两个实例给出了串口读和写的两个程序的  main函数部分,这里用到的函数有前面讲述到的 open_port 和 set_opt 函数。

 

/*写串口程序*/

#include <stdio.h>

#include <string.h>

#include <sys/types.h>

#include <errno.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

#include <termios.h>

#include <stdlib.h>



/*读串口程序*/

int main(void)

{

     int fd;

     int nread,i;

     char buff[]="Hello\n";



     if((fd=open_port(fd,1))<0){//打开串口

    perror("open_port error");

    return;

     }

     if((i=set_opt(fd,115200,8,'N',1))<0){//设置串口

    perror("set_opt error");

    return;

}

printf("fd=%d\n",fd);

     nread=read(fd,buff,8);//读串口

     printf("nread=%d,%s\n",nread,buff);

     close(fd);

     return;

}

 

读者可以将该程序在宿主机上运行,然后用串口线将目标板和宿主机连接起来,之后将目标板上电,这样就可以看到宿主机上有目标板的串口输出。

 

[root@localhost file]# ./receive

fcntl=0

isatty success!

fd-open=3

set done

fd=3

nread=8,…

 

另外,读者还可以考虑一下如何使用 select 函数实现串口的非阻塞读写,具体实例会在后面的实验中给出。

6.5  标准 I/O 开发

本章前面几节所述的文件及 I/O 读写都是基于文件描述符的。这些都是基本的 I/O 控制,是不带缓存的。而本节所要讨论的 I/O 操作都是基于流缓冲的,它是符合 ANSI C 的标准 I/O处理,这里有很多函数读者已经非常熟悉了(如printf、scantf函数等),因此本节中仅简要介绍最主要的函数。

标准 I/O 提供流缓冲的目的是尽可能减少使用 read 和 write 调用的数量。标准 I/O 提供了3 种类型的缓冲存储。

 全缓冲。在这种情况下,当填满标准 I/O 缓存后才进行实际 I/O 操作。对于驻在磁盘上的文件通常是由标准 I/O 库实施全缓冲的。在一个流上执行第一次 I/O 操作时,通常调用malloc 就是使用全缓冲。

 行缓冲。在这种情况下,当在输入和输出中遇到新行符时,标准 I/O 库执行 I/O 操作。

这允许我们一次输出一个字符(如 fputc 函数),但只有写了一行之后才进行实际 I/O 操作。当流涉及一个终端时(例如标准输入和标准输出),典型地使用行缓冲。

 不带缓冲。标准 I/O 库不对字符进行缓冲。如果用标准 I/O 函数写若干字符到不带缓冲的流中,则相当于用 write系统的调用函数将这些字符写全相比较的打开文件上。标准出错情况 stderr 通常是不带缓存后,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个新行字符。

在下面讨论具体函数时,请读者注意区分这 3 种不同的情况。

6.5.1 打开和关闭文件

1.打开文件

(1)函数说明

打开文件有三个标准函数,分别为:fopen、fdopen 和 freopen。它们可以以不同的模式打开,但都返回一个指向 FILE 的指针,该指针与将对应的 I/O 流相绑定了。此后,对文件的读写都是通过这个FILE指针来进行。其中  可以指定打开文件的路径和模式,fdopen可以指定打开的文件描述符和模式,而   freopen除可指定打开的文件、模式外,还可指定特定的 IO 流。

(2)函数格式定义

fopen 函数格式如表 6.13 所示。

这里的 mode 类似于 open 中的 flag,可以定义打开文件的具体权限等,表 6.14 说明了 fopen中 mode 的各种取值。

 

注意在每个选项中加入 b 字符用来告诉函数库打开的文件为二进制文件,而非纯文字文件。不过在 Linux 系统中会自动识别不同类型的文件而将此符号忽略。

fdopen 函数格式如表 6.15 所示。

表   fdopen 函数语法要点

 

freopen 函数格式如表 6.16 所示。

 

2.关闭文件

(1)函数说明

关闭标准流文件的函数为fclose,这时缓冲区内的数据写入文件中,并释放系统所提供的文件资源。

(2)函数格式说明

fclose函数格式如表 6.17 所示。

3.使用实例

文件打开关闭的操作都比较简单,这里仅以 fopen 和 fclose 为例,代码如下所示:

 

/*fopen.c*/

#include <stdio.h>

main()

{

     FILE *fp;

int c;

/*调用fopen 函数*/

if((fp=fopen("exist","w"))!=NULL){

    printf("open success!");

     }

     fclose(fp);

}

读者可以尝试用其他文件打开函数进行练习。

6.5.2    文件读写

1.读文件

(1)fread 函数说明

在文件流打开之后,可对文件流进行读写等操作,其中读操作的函数为 fread。

(2)fread 函数格式fread 函数格式如表 6.18 所示。


2.写文件

(1)fwrite 函数说明

fwrite 函数是用于对指定的文件流进行写操作。

(2)fwrite 函数格式

fwrite 函数格式如表 6.19 所示。

 

这里仅以 fwrite 为例简单说明:

 

/*fwrite.c*/

#include <stdio.h>

int main()

{

     FILE *stream;

     char s[3]={'a','b','c'};

/*首先使用 fopen 打开文件,之后再调用 fwrite 写入文件*/

     stream=fopen("what","w");

     i=fwrite(s,sizeof(char),nmemb,stream);

     printf("i=%d",i);

     fclose(stream);

}

 

运行结果如下所示:

[root@localhost file]# ./write

i=3

[root@localhost file]# cat what

abc

6.5.3    输入输出

文件打开之后,根据一次读写文件中字符的数目可分为字符输入输出、行输入输出和格式化输入输出,下面分别对这 3 种不同的方式进行讨论。

1.字符输入输出

字符输入输出函数一次仅读写一个字符。其中字符输入输出函数如表 6.20 和表 6.21 所示。


这几个函数功能类似,其区别仅在于 getc 和 putc 通常被实现为宏,而 fgetc 和 fputc 不能实现为宏,因此,函数的实现时间会有所差别。

下面这个实例结合 fputc 和 fgetc,将标准输入复制到标准输出中去。

 

/*fput.c*/

#include<stdio.h>

main()

{

     int c;

/*fgetc 的结果作为fputc 的输入*/

fputc(fgetc(stdin),stdout);

}

 

运行结果如下所示:

[root@localhost file]# ./file

w(用户输入)

w(屏幕输出)

2.行输入输出

行输入输出函数一次操作一行。其中行输入输出函数如表 6.22 和表 6.23 所示。

这里以 gets 和 puts 为例进行说明,本实例将标准输入复制到标准输出,如下所示:

/*gets.c*/

#include<stdio.h>

main()

{

     char s[80];

/*同上例,把 fgets 的结果作为 fputs 的输入*/

fputs(fgets(s,80,stdin),stdout);

}

 

 

运行该程序,结果如下所示:

[root@130 test]# ./file2

This is stdin(用户输入)

This is stdin(屏幕输出)


3.格式化输入输出

格式化输入输出函数可以指定输入输出的具体格式,这里有读者已经非常熟悉的 printf、scanf 等函数,这里就简要介绍一下它们的格式。如下表 6.24~表 6.26 所示。

 

 

由于本节的函数用法比较简单,并且比较常用,因此就不再举例了,请读者需要用到时自行查找其用法。

6.6  实验内容

6.6.1  文件读写及上锁

1.实验目的

通过编写文件读写及上锁的程序,进一步熟悉 Linux 中文件 I/O 相关的应用开发,并且熟练掌握 open、read、write、fcntl 等函数的使用。


2.实验内容

该实验要求首先打开一个文件,然后将该文件上写入锁,并写入   hello  字符串。接着在解锁后再将该文件上读取锁,并读取刚才写入的内容。最后模拟多进程,同时读写一个文件时的情况。


3.实验步骤

(1)画出实验流程图

该实验流程图如图 6.4 所示。

(2)编写代码

该实验源代码如下所示,其中用到的 lock_set函数可参见第6.3.3节。

 

/*expr1.c 实验一源码*/

#include <unistd.h>

#include <sys/file.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

void lock_set(int fd,int type);


int main(void)

{

     int fd,nwrite,nread,len;

     char *buff="Hello\n";

     char buf_r[100];

     len=strlen(buff);

     fd=open("hello",O_RDWR | O_CREAT, 0666);

     if(fd < 0){

   perror("open");

   exit(1);

     }

/*加上写入锁*/

lock_set(fd, F_WRLCK);

     if((nwrite=write(fd,buff,len))==len){

   printf("write success\n");

     }

     getchar();

/*解锁*/

lock_set(fd, F_UNLCK);

     getchar();

/*加上读取锁*/

lock_set(fd, F_RDLCK);

lseek(fd,0,SEEK_SET);

     if((nread=read(fd,buf_r,len))==len){

   printf("read:%s\n",buf_r);

     }

     getchar();

/*解锁*/

lock_set(fd, F_UNLCK);

     getchar();

     close(fd);

     exit(0);

}

 

 

(3)首先在宿主机上编译调试该程序,如下所示:

[root@localhost process]# gcc expr1.c –o expr1

(4)在确保没有编译错误后,使用交叉编译该程序,如下所示:

[root@localhost process]# arm-linux-gcc expr1.c  –o expr2

(5)将生成的可执行程序下载到目标板上运行。


4.实验结果

此实验在目标板上的运行结果如下所示:

 

[root@(none) 1]# ./expr1

write lock set by 75

write success



release lock by 75


read lock set by 75

read:Hello



release lock by 75

 


另外,在本机上可以开启两个终端,同时运行该程序。实验结果会和这两个进程运行过程具体相关,希望读者能具体分析每种情况。下面列出其中一种情况:终端一:

[root@localhost file]# ./expr1

write lock set by 3013

write success


release lock by 3013



read lock set by 3013

read:Hello



release lock by 3013

 

终端二:

[root@localhost file]# ./expr1

write lock already set by 3013



write lock set by 3014

write success


release lock by 3014



read lock set by 3014

read:Hello


release lock by 3014

6.6.2  多路复用式串口读写

1.实验目的

通过编写多路复用式串口读写,进一步理解 select 函数的作用,同时更加熟练掌握 Linux设备文件的读写方法。

2.实验内容

完成串口读写操作,这里设定从串口读取消息时使用 select 函数,发送消息的程序不需要用 select 函数,只发送“Hello”消息由接收端接收。

3.实验步骤

(1)画出流程图

下图 6.5 是读串口的流程图,写串口的流程图与此类似。

 

(2)编写代码

分 别 编 写串 口 读写 程序 , 该程 序 中用 到 的open_port 和 set_opt 函数请参照 6.4 节所述。

写串口程序的代码如下所示:

/*写串口*/

#include <stdio.h>

#include <string.h>

#include <sys/types.h>

#include <errno.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

#include <termios.h>

#include <stdlib.h>

int main(void)

{

     int fd;

     int nwrite,i;

     char buff[]="Hello\n";

/*打开串口*/

     if((fd=open_port(fd,1))<0){

    perror("open_port error");

    return;

     }

/*设置串口*/

     if((i=set_opt(fd,115200,8,'N',1))<0){

    perror("set_opt error");

    return;

     }

     printf("fd=%d\n",fd);

/*向串口写入字符串*/

     nwrite=write(fd,buff,8);

     printf("nwrite=%d\n",nwrite);

     close(fd);

     return;

}

 

 

读串口程序的代码如下所示:

/*读串口*/

#include <stdio.h>

#include <string.h>

#include <sys/types.h>

#include <errno.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

#include <termios.h>

#include <stdlib.h>

int main(void)

{

     int fd;

     int nread,nwrite,i;

     char buff[8];

     fd_set rd;

/*打开串口*/

     if((fd=open_port(fd,1))<0){

    perror("open_port error");

    return;

     }

/*设置串口*/

     if((i=set_opt(fd,115200,8,'N',1))<0){

    perror("set_opt error");

    return;

     }

/*利用 select 函数来实现多个串口的读写*/

     FD_ZERO(&rd);

     FD_SET(fd,&rd);

     while(FD_ISSET(fd,&rd)){

     if(select(fd+1,&rd,NULL,NULL,NULL)<0)

  perror("select");

     else{

    while((nread = read(fd, buff, 8))>0)

    {

  printf("nread=%d,%s\n",nread,buff);

    }

     }

     close(fd);

     return;

}

 

(3)接下来把第一个写串口程序交叉编译,再把第二个读串口程序在 PC 机上编译,分别得到可执行文件 write 和 read。

(4)将写串口程序下载到开发板上,然后连接 PC 和开发板的串口 1。首先运行读串口程序,再运行写串口程序。

4.实验结果

发送端的运行结果如下所示:

 

[root@(none) 1]# ./write

fcntl=0

isatty success!

fd-open=3

set done

fd=3

nwrite=8

 

接收端的运行结果如下所示:

 

[root@localhost file]# ./read

fcntl=0

isatty success!

fd-open=3

set done

fd=3

nread=8,Hello!

 

读者还可以尝试修改 select 函数选项,例如设定一个等待时间,查看程序的运行结果。

思考与练习

使用 select 函数实现 3 个串口的通信:串口 1 接收数据,串口 2 和串口 3 向串口 1 发送数据。

第7章 进程控制开发

7.1  Linux 下进程概述

7.1.1  进程相关基本概念

1.进程的定义

进程是一个程序的一次执行的过程。它和程序是有本质区别的,程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念;而进程是一个动态的概念,它是程序执行的过程,包括了动态创建、调度和消亡的整个过程。它是程序执行和资源管理的最小单位。因此,对系统而言,当用户在系统中键入命令执行一个程序的时候,它将启动一个进程。

2.进程控制块

进程是 Linux 系统的基本调度单位,那么从系统的角度看如何描述并表示它的变化呢?在这里,是通过进程控制块来描述的。进程控制块包含了进程的描述信息、控制信息以及资源信息,它是进程的一个静态描述。在 Linux 中,进程控制块中的每一项都是一个 task_struct结构,它是在include/linux/sched.h 中定义的。

3.进程的标识

在 Linux 中最主要的进程标识有进程号(PID,Process Idenity Number)和它的父进程号(PPID,parent process ID)。其中 PID 惟一地标识一个进程。PID 和 PPID 都是非零的正整数。

在 Linux 中获得当前进程的 PID 和 PPID 的系统调用函数为 getpid 和 getppid,通常程序获得当前进程的 PID 和 PPID 可以将其写入日志文件以做备份。getpid 和 getppid 系统调用过程如下所示:

/*process.c*/

#include<stdio.h>

#include<unistd.h>

#include <stdlib.h>

int main()

{

/*获得当前进程的进程 ID 和其父进程 ID*/

    printf("The PID of this process is %d\n",getpid());

    printf("The PPID of this process is %d\n",getppid());

}

使用 arm-linux-gcc 进行交叉编译,再将其下载到目标板上运行该程序,可以得到如下结果,该值在不同的系统上会有所不同:

[root@localhost process]# ./process

The PID of this process is 78

THe PPID of this process is 36

另外,进程标识还有用户和用户组标识、进程时间、资源利用情况等


4.进程运行的状态

进程是程序的执行过程,根据它的生命期可以划分成 3 种状态。

执行态:该进程正在,即进程正在占用 CPU。

就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间片。

等待态:进程不能使用 CPU,若等待事件发生则可将其唤醒。

它们之间转换的关系图如图 7.1 所示。


7.1.2  Linux 下的进程结构

Linux  系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。也就是说,进程之间是分离的任务,拥有各自的权利和责任。其中,每一个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生异常,它也不会影响到系统中的其他进程。

Linux 中的进程包含 3 个段,分别为“数据段”、“代码段”和“堆栈段”

“数据段”存放的是全局变量、常数以及动态数据分配的数据空间(如 malloc函数取得的空间)等。

“代码段”存放的是程序代码的数据。

“堆栈段”存放的是子程序的返回地址、子程序的参数以及程序的局部变量。如图7.2 所示。

7.1.3  Linux 下进程的模式和类型

在 Linux  系统中,进程的执行模式划分为用户模式和内核模式。如果当前运行的是用户程序、应用程序或者内核之外的系统程序,那么对应进程就在用户模式下运行;如果在用户程序执行过程中出现系统调用或者发生中断事件,那么就要运行操作系统(即核心)程序,进程模式就变成内核模式。在内核模式下运行的进程可以执行机器的特权指令,而且此时该进程的运行不受用户的干扰,即使是 root 用户也不能干扰内核模式下进程的运行。

用户进程既可以在用户模式下运行,也可以在内核模式下运行,如图 7.3 所示。

 

7.1.4  Linux 下的进程管理

Linux 下的进程管理包括启动进程和调度进程,下面就分别对这两方面进行简要讲解。

1.启动进程

Linux  下启动一个进程有两种主要途径:手工启动和调度启动。手工启动是由用户输入命令直接启动进程,而调度启动是指系统根据用户的设置自行启动进程。

(1)手工启动

手工启动进程又可分为前台启动和后台启动。

 前台启动是手工启动一个进程的最常用方式。一般地,当用户键入一个命令如“ls -l”时,就已经启动了一个进程,并且是一个前台的进程

 后台启动往往是在该进程非常耗时,且用户也不急着需要结果的时候启动的。比如用户要启动一个需要长时间运行的格式化文本文件的进程。为了不使整个 shell 在格式化过程中都处于“瘫痪”状态,从后台启动这个进程是明智的选择。

(2)调度启动

有时,系统需要进行一些比较费时而且占用资源的维护工作,并且这些工作适合在深夜无人职守的时候进行,这时用户就可以事先进行调度安排,指定任务运行的时间或者场合,到时候系统就会自动完成这一切工作。

使用调度启动进程有几个常用的命令,如 at 命令在指定时刻执行相关进程,cron 命令可以自动周期性地执行相关进程,在需要使用时读者可以查看相关帮助手册。

2.调度进程

调度进程包括对进程的中断操作、改变优先级、查看进程状态等,在 Linux 下可以使用相关的系统命令实现其操作,下表列出了 Linux 中常见的调用进程的系统命令,读者在需要的时候可以自行查找其用法。

7.2  Linux 进程控制编程

进程创建

1.fork()

在 Linux 中创建一个新进程的惟一方法是使用 fork 函数。fork 函数是 Linux 中一个非常重要的函数,和读者以往遇到的函数也有很大的区别,它执行一次却返回两个值。希望读者能认真地学习这一部分的内容。

(1)fork 函数说明

fork 函数用于从已存在进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。这两个分别带回它们各自的返回值,其中父进程的返回值是子进程的进程号,而子进程则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。

使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。因此可以看出,使用 fork 函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得 fork 函数的执行速度并不很快。

(2)fork 函数语法

表 7.2 列出了 fork 函数的语法要点。

 

(3)fork 函数使用实例

/*fork.c*/

#include <sys/types.h>

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>



int main(void)

{

    pid_t result;


    /*调用 fork 函数,其返回值为

    result = fork();


    /*通过 result 的值来判断 fork 函数的返回情况,首先进行出错处理*/

    if(result == -1)

    {

        perror("fork");

        exit(1);

    }

    else if(result == 0) /*返回值为 0 代表子进程*/

    {

        printf("The  return  value  is  %d\nIn  child  process!!\nMy  PID  is %d\n",result,getpid());

    }

    else

    {

        printf("The  return  value  is  %d\nIn  father  process!!\nMy  PID  is%d\n",result,getpid());

    }

}

 

[root@localhost process]#  –o fork

 

将可执行程序下载到目标板上,运行结果如下所示:

The return valud is 76

In father process!!

My PID is 75

The return value is :0

In child process!!

My PID is 76



从该实例中可以看出,使用fork函数新建了一个子进程,其中的父进程返回子进程的PID,而子进程的返回值为 0。

(4)函数使用注意点

fork 函数使用一次就创建一个进程,所以若把 fork 函数放在了 if else 判断语句中则要小心,不能多次使用 fork 函数。

由于 fork 完整地拷贝了父进程的整个地址空间,因此执行速度是比较慢的。为了加快 fork 的执行速度,有些 UNIX 系统设计者创建了 vfork。vfork 也能创建新进程,但它不产生父进程的副本。它是通过允许父子进程可访问相同物理内存从而伪装了对进程地址空间的真实拷贝,当子进程需要改变内存中数据时才拷贝父进程。这就是著名的“写操作时拷贝”(copy-on-write)技术。

现在很多嵌入式 Linux 系统的 fork 函数调用都采用 vfork 函数的实现方式,实际上 uClinux 所有的多进程管理都通过 vfork 来实现。

 

2.exec 函数族

(1)exec 函数族说明

fork 函数是用于创建一个子进程,该子进程几乎拷贝了父进程的全部内容,但是,这个新创建的进程如何执行呢?这个exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也可以是 Linux 下任何可执行的脚本文件。

在 Linux 中使用 exec 函数族主要有两种情况:

 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何 exec 函数族让自己重生;

 如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程。(这种情况非常普遍)

(2)exec 函数族语法

实际上,在 Linux 中并没有 exec()函数,而是有 6 个以 exec 开头的函数族,它们之间语法有细微差别,本书在下面会详细讲解。

下表 7.3 列举了 exec 函数族的 6 个成员函数的语法。

 

这 6 个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这几个方面进行比较。

 查找方式

读者可以注意到,表 7.3 中的前 4 个函数的查找方式都是完整的文件目录路径,而最后2 个函数(也就是以 p 结尾的两个函数)可以只给出文件名,系统就会自动从环境变量“$PATH”所指出的路径中进行查找。

 参数传递方式

exec 函数族的参数传递有两种方式:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。

在这里是以函数名的第 5 位字母来区分的,字母为“l”(list)的表示逐个列举的方式,其语法为 char *arg;字母为“v”(vertor)的表示将所有参数整体构造指针数组传递,其语法为*const argv[]。读者可以观察 execl、execle、execlp 的语法与 execv、execve、execvp 的区别。它们具体的用法在后面的实例讲解中会举例说明。

这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以    NULL 表示结束,如果使用逐个列举方式,那么要把它强制转化成一个字符指针,否则 exec 将会把它解释为一个整型参数,如果一个整型数的长度 char *的长度不同,那么 exec 函数就会报错。

 exec函数族可以默认系统的环境变量,也可以传入指定的环境变量。这里以“e”(Enviromen)结尾的两个函数 execle、execve 就可以在envp[]中指定当前进程所使用的环境变量。

下表 7.4 再对这 4 个函数中函数名和对应语法做一总结,主要指出了函数名中每一位所表明的含义,希望读者结合此表加以记忆。

 

 

(3)exec 使用实例

下面的第一个示例说明了如何使用文件名的方式来查找可执行文件,同时使用参数列表的方式。这里用的函数是 execlp。

 

/*execlp.c*/

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

 

 

int main()

{

  if(fork()==0)

  {

      /*调用 execlp 函数,这里相当于调用了"ps -ef"命令*/

      if(execlp("ps","ps","-ef",NULL)<0)

          perror("execlp error!");

  }

}


在该程序中,首先使用 fork 函数新建一个子进程,然后在子进程里使用 execlp 函数。读者可以看到,这里的参数列表就是在 shell 中使用的命令名和选项。并且当使用文件名的方式进行查找时,系统会在默认的环境变量 PATH 中寻找该可执行文件。读者可将编译后的结果下载到目标板上,运行结果如下所示:

[root@(none) 1]# ./execlp

  PID TTY     Uid        Size State Command

    1         root       1832     S    init

2         root                   0     S    [keventd]

    3         root                   0     S    [ksoftirqd_CPU0]

    4         root                   0     S    [kswapd]

    5         root                   0     S    [bdflush]

    6         root                   0     S    [kupdated]

    7         root                   0     S    [mtdblockd]

    8         root                   0     S    [khubd]

   35         root       2104     S    /bin/bash /usr/etc/rc.local

   36         root       2324     S    /bin/bash

   41         root       1364     S    /sbin/inetd

   53         root      14260     S    /Qtopia/qtopia-free-1.7.0/bin/qpe -qws

   54         root      11672     S    quicklauncher

   65         root                    0     S    [usb-storage-0]

   66         root                    0     S    [scsi_eh_0]

   83         root       2020     R    ps -ef

[root@(none) /]# env

PATH=/Qtopia/qtopia-free-1.7.0/bin:/usr/bin:/bin:/usr/sbin:/sbin

 

 

此程序的运行结果与在 Shell 中直接键入命令“ps  -ef”是一样的,当然,在不同的系统不同时刻都可能会有不同的结果。

接下来的示例 2 使用完整的文件目录来查找对应的可执行文件。注意目录必须以“/”开头,否则将其视为文件名。

 


/*execl.c*/

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>


int main()

{

      if(fork()==0){

/*调用 execl 函数,注意这里要给出 ps 程序所在的完整路径*/

  if(execl("/bin/ps","ps","-ef",NULL)<0)

    perror("execl error!");

      }

}

 

同样下载到目标板上运行,运行结果同上例,如下所示:

[root@(none) 1]# ./execl

PID TTY         Uid        Size State Command

    1             root       1832     S    init

    2             root          0     S    [keventd]

    3             root         0     S    [ksoftirqd_CPU0]

    4             root         0     S    [kswapd]

    5             root         0     S    [bdflush]

    6             root         0     S    [kupdated]

 

 

示例 3 利用函数 execle,将环境变量添加到新建的子进程中去,这里的“env”是查看当前进程环境变量的命令,如下所示:

 

/*execle*/

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>


int main()

{

    /*命令参数列表,必须以 NULL 结尾*/

    char *envp[]={"PATH=/tmp","USER=ateng",NULL};


    if(fork()==0)

    {

        /*调用 execle 函数,注意这里也要指出 env 的完整路径*/

        if(execle("/bin/env","env",NULL,envp)<0)

            perror("execle error!");

    }

}

 

下载到目标板后的运行结果如下所示:

 

[root@(none) 1]#./execle

PATH=/tmp

USER=ateng

 

最后一个示例使用 execve 函数,通过构造指针数组的方式来传递参数,注意参数列表一定要以 NULL 作为结尾标识符。其代码和运行结果如下所示:

 

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>


int main()

{

/*命令参数列表,必须以 NULL 结尾*/

char *arg[]={"env",NULL};

char *envp[]={"PATH=/tmp","USER=ateng",NULL};

      if(fork()==0){

    if(execve("/bin/env",arg,,envp)<0)

      perror("execve error!");

      }

}

 

下载到目标板后的运行结果如下所示:

 

[root@(none) 1]# ./execve

PATH=/tmp

USER=ateng

 

 

(4)exec 函数族使用注意点

在使用 exec 函数族时,一定要加上错误判断语句。因为 exec 很容易执行失败,其中最常见的原因有:

 找不到文件或路径,此时 errno 被设置为 ENOENT;

 数组 argv 和 envp 忘记用 NULL 结束,此时 errno 被设置为 EFAULT;

 没有对应可执行文件的运行权限,此时 errno 被设置为 EACCES。

事实上,这 6 个函数中真正的系统调用只有 execve,其他 5 个都是库函数,它们最终都会调用 execve 这个系统调用。

3.exit 和_exit

(1)exit 和_exit 函数说明

exit 和_exit 函数都是用来终止进程的。当程序执行到 exit 或_exit 时,进程会无条件地停止剩下的所有操作,清除包括 PCB 在内的各种数据结构,并终止本进程的运行。但是,这两个函数还是有区别的,这两个函数的调用过程如图 7.4 所示。

 

从图中可以看出,_exit()函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构;exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序。exit()函数与_exit()函数最大的区别就在于 exit()函数在调用 exit 系统之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理 I/O 缓冲”一项。

由于在 Linux 的标准函数库中,有一种被称作“缓冲 I/O(buffered I/O)”操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。

这种技术大大增加了文件读写的速度,但也为编程带来了一点麻烦。比如有一些数据,认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失。因此,若想保证数据的完整性,就一定要使用 exit()函数。

(2)exit 和_exit 函数语法

下表 7.5 列出了 exit 和_exit 函数的语法规范。

(3)exit 和_exit 使用实例:

这两个示例比较了 exit 和_exit 两个函数的区别。由于 printf 函数使用的是缓冲 I/O 方式,该函数在遇到“\n”换行符时自动从缓冲区中将记录读出。示例中就是利用这个性质来进行比较的。以下是示例 1 的代码:

 

/*exit.c*/

#include <stdio.h>

#include <stdlib.h>


int main()

{

      printf("Using exit...\n");

      printf("This is the content in buffer");

exit(0);

}

[root@(none) 1]# ./exit

Using exit...

This is the content in buffer[root@(none) 1]#

 

 

读者从输出的结果中可以看到,调用 exit 函数时,缓冲区中的记录也能正常输出。

以下是示例 2 的代码:

 

 

/*_exit.c*/

#include <stdio.h>

#include <unistd.h>

int main()

{

      printf("Using _exit...\n");

printf("This is the content in buffer");

_exit(0);

}

[root@(none) 1]#            ./_exit

Using _exit...

[root@(none) 1]#

 

读者从最后的结果中可以看到,调用_exit 函数无法输出缓冲区中的记录。

在一个进程调用了exit之后,该进程并不马上就完全消失,而是留下一个称为僵尸进程(Zombie)的数据结构。僵尸进程是一种非常特殊的进程,它几乎已经放弃了所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。

4.wait 和

(1)wait 和 waitpid 函数说明

wait 函数是用于使父进程(也就是调用 wait 的进程)阻塞,直到一个子进程结束或者该进程接到了一个指定的信号为止。如果该父进程没有子进程或者他的子进程已经结束,则 wait就会立即返回。

waitpid 的作用和 wait 一样,但它并不一定要等待第一个终止的子进程,它还有若干选项,如可提供一个非阻塞版本的 wait 功能,也能支持作业控制。实际上 wait 函数只是 waitpid 函数的一个特例,在 Linux 内部实现 wait 函数时直接调用的就是 waitpid 函数。

(2)wait 和 waitpid 函数格式说明

表 7.6 列出了 wait 函数的语法规范。

 

 

下表 7.7 列出了 waitpid 函数的语法规范。

(3)waitpid 使用实例

由于 wait 函数的使用较为简单,在此仅以 waitpid 为例进行讲解。本例中首先使用 fork新建一子进程,然后让其子进程暂停    5s(使用了 sleep  函数)。接下来对原有的父进程使用waitpid    函数,并使用参数 使该父进程不会阻塞。若有子进程退出,则waitpid返回子进程号;若没有子进程退出,则 waitpid 返回 0,并且父进程每隔一秒循环判断一次。

该程的流程图如图 7.5 所示。

 

该程序源代码如下所示:

 

/*waitpid.c*/

#include <sys/types.h>

#include <sys/wait.h>

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

int main()

{

    pid_t pc,pr;


    pc=fork();

    if(pc<0)

        printf("Error fork.\n");

    else if(pc==0)  /*子进程*/

    {

        /*子进程暂停

        sleep(5);


        /*子进程正常退出*/

        exit(0);

    }

    else /*父进程*/

    {

        /*循环测试子进程是否退出*/

        do

        {

            /*调用 waitpid,且父进程不阻塞*/

            pr=waitpid(pc,NULL,WNOHANG);

            /*若子进程还未退出,则父进程暂停

            if(pr==0)

            {

                printf("The child process has not exited\n");

                sleep(1);

            }

        }while(pr==0);


        /*若发现子进程退出,打印出相应情况*/

        if(pr==pc)

            printf("Get child %d\n",pr);

        else

            printf("some error occured.\n");

    }

}

 

将该程序交叉编译,下载到目标板后的运行情况如下所示:

 

[root@(none) 1]#./waitpid

The child process has not exited

The child process has not exited

The child process has not exited

The child process has not exited

The child process has not exited

Get child 75

 

可见,该程序在经过 5 次循环之后,捕获到了子进程的退出信号,具体的子进程号在不同的系统上会有所区别。

读者还可以尝试把“pr=waitpid(pc,NULL,WNOHANG);”这句改为“pr=waitpid(pc,NULL, 0);”和“pr=wait(NULL);”,运行的结果为:

[root@(none) 1]# ./waitpid

Get child 76

可见,在上述两种情况下,父进程在调用 waitpid 或 wait 之后就将自己阻塞,直到有子进程退出为止。

7.3  Linux 守护进程

7.3.1    守护进程概述

守护进程,也就是通常所说的 Daemon 进程,是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止。Linux 系统有很多守护进程,大多数服务都是通过守护进程实现的,如本书在第二章中讲到的系统服务都是守护进程。同时,守护进程还能完成许多系统任务,例如,作业规划进程 crond、打印进程 lqd等(这里的结尾字母 d 就是 Daemon 的意思)。

由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才会退出。如果想让某个进程不因为用户或终端或其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。可见,守护进程是非常重要的。

7.3.2  编写守护进程

编写守护进程看似复杂,但实际上也是遵循一个特定的流程。只要将此流程掌握了,就能很方便地编写出用户自己的守护进程。下面就分 4 个步骤来讲解怎样创建一个简单的守护进程。在讲解的同时,会配合介绍与创建守护进程相关的几个系统函数,希望读者能很好地掌握。

1.创建子进程,父进程退出

这是编写守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在    Shell   终端里造成一程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在 Shell 终端里则可以执行其他的命令,从而在形式上做到了与控制终端的脱离。

到这里,有心的读者可能会问,父进程创建了子进程,而父进程又退出之后,此时该子进程不就没有父进程了吗?守护进程中确实会出现这么一个有趣的现象,由于父进程已经先于子进程退出,会造成子进程没有父进程,从而变成一个孤儿进程。在 Linux 中,每当系统发现一个孤儿进程,就会自动由 1 号进程(也就是 init 进程)收养它,这样,原先的子进程就会变成 init 进程的子进程了。其关键代码如下所示:

 

/*父进程退出*/

pid=fork();

if(pid>0){

exit(0);

}

 

2.在子进程中创建新会话

这个步骤是创建守护进程中最重要的一步,虽然它的实现非常简单,但它的意义却非常重大。在这里使用的是系统函数 setsid,在具体介绍 setsid 之前,读者首先要了解两个概念:进程组和会话期。

 进程组

进程组是一个或多个进程的集合。进程组由进程组 ID 来惟一标识。除了进程号(PID)之外,进程组 ID 也一个进程的必备属性。

每个进程组都有一个组长进程,其组长进程的进程号等于进程组 ID。且该进程 ID 不会因组长进程的退出而受到影响。

 会话期

会话组是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期,它们之间的关系如下图 7.6 所示。

接下来就可以具体介绍 setsid 的相关内容:

(1)setsid 函数作用

setsid 函数用于创建一个新的会话,并担任该会话组的组长。调用 setsid 有下面的 3 个作用。

让进程摆脱原会话的控制。

让进程摆脱原进程组的控制。

 让进程摆脱原控制终端的控制。

 

那么,在创建守护进程时为什么要调用 setsid 函数呢?读者可以回忆一下创建守护进程的第一步,在那里调用了 fork 函数来创建子进程再将父进程退出。由于在调用 fork 函数时,子进程全盘拷贝了父进程的进程会话期、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,还不是真正意义上独立开来,而 setsid 函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

(2)setsid 函数格式

下表 7.8 列出了 setsid 函数的语法规范

 

3.改变当前目录为根目录

这一步也是必要的步骤。使用 fork 创建的子进程继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统(比如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是 chdir

 

4.重设文件权限掩码

文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有一个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用 fork 函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为    0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为 umask(0)

 

5.关闭文件描述符

同文件权限掩码一样,用 fork 函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如 printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为 0、1 和 2 的 3 个文件(常说的输入、输出和报错这 3 个文件)已经失去了存在的价值,也应被关闭。通常按如下方式关闭文件描述符:

 

for(i=0;i<MAXFILE;i++)

      close(i);

 

这样,一个简单的守护进程就建立起来了,创建守护进程的流程图如图 7.7 所示。

 

 

下面是实现守护进程的一个完整实例:

该实例首先建立了一个守护进程,然后让该守护进程每隔 10s 在/tmp/dameon.log 中写入一句话。

/*dameon.c 创建守护进程实例*/

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<fcntl.h>

#include<sys/types.h>

#include<unistd.h>

#include<sys/wait.h>



#define MAXFILE 65535

int main()

{

  pid_t pc;

  int i,fd,len;

  char *buf="This is a Dameon\n";


  len =strlen(buf);

  pc=fork(); //第一步

  if(pc<0)

  {

      printf("error fork\n");

      exit(1);

  }

  else if(pc>0)

      exit(0);


  /*第二步*/

  setsid();


  /*第三步*/

  chdir("/");


  /*第四步*/

  umask(0);


  for(i=0;i<MAXFILE;i++)/*第五步*/

      close(i);


  /*这时创建完守护进程,以下开始正式进入守护进程工作*/

  while(1)

  {

      if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0)

      {

          perror("open");

          exit(1);

      }

      write(fd, buf, len+1);

      close(fd);

      sleep(10);

  }

}

 

将该程序下载到开发板中,可以看到该程序每隔 10s 就会在对应的文件中输入相关内容。并且使用 ps 可以看到该进程在后台运行。如下所示:

 

[root@(none) 1]# tail -f /tmp/dameon.log

This is a Dameon

This is a Dameon

This is a Dameon

This is a Dameon

[root@(none) 1]# ps -ef|grep daemon

    76             root           1272   S   ./daemon

    85             root           1520   S   grep daemon

7.3.3    守护进程的出错处理

读者在前面编写守护进程的具体调试过程中会发现,由于守护进程完全脱离了控制终端,因此,不能像其他进程的程序一样通过输出错误信息到控制终端来通知程序员,即使使用gdb也无法正常调试。那么,守护进程的进程要如何调试呢?一种通用的办法是使用syslog服务,将程序中的出错信息输入到“/var/log/messages”系统日志文件中,从而可以直观地看到程序的问题所在。

注意   “/var/log/message”系统日志文件只能由拥有 root 权限的超级用户查看。

Syslog 是 Linux 中的系统日志管理服务,通过守护进程 syslogd 来维护。该守护进程在启动时会读一个配置文件“/etc/syslog.conf”。该文件决定了不同种类的消息会发送向何处。例如,紧急消息可被送向系统管理员并在控制台上显示,而警告消息则可记录到一个文件中。

该机制提供了 3 个 syslog 函数,分别为 openlog、syslog 和 closelog。下面就分别介绍这3 个函数。

(1)syslog 函数说明

通常,openlog 函数用于打开系统日志服务的一个连接;syslog 函数是用于向日志文件中写入消息,在这里可以规定消息的优先级、消息输出格式等;closelog  函数是用于关闭系统日志服务的连接。

(2)syslog 函数格式

下表 7.9 列出了 openlog 函数的语法规范

表 7.10 列出了 syslog 函数的语法规范。

 

 

表 7.11 列出了 closelog 函数的语法规范


(3)使用实例

这里将上一节中的示例程序用 syslog 服务进行重写,其中有区别的地方用加粗的字体表示,源代码如下所示:

 

/*syslog_dema.c 利用 syslog 服务的守护进程实例*/

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<fcntl.h>

#include<sys/types.h>

#include<unistd.h>

#include<sys/wait.h>

#include<syslog.h>



#define MAXFILE 65535

int main()

{

    pid_t pc,sid;

    int i,fd,len;

    char *buf="This is a Dameon\n";


    len =strlen(buf);

    pc=fork();

    if(pc<0)

    {

        printf("error fork\n");

        exit(1);

    }

    else if(pc>0)

        exit(0);


    /*打开系统日志服务,openlog*/

    openlog("demo_update",LOG_PID, LOG_DAEMON);

    if((sid=setsid())<0)

    {

        syslog(LOG_ERR, "%s\n", "setsid");

        exit(1);

    }


    if((sid=chdir("/"))<0)

    {

        syslog(LOG_ERR, "%s\n", "chdir");

        exit(1);

    }


    umask(0);

    for(i=0;i<MAXFILE;i++)

        close(i);


    while(1)

    {

        /*打开守护进程的日志文件,并写入 open 的日志记录*/

        if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND, 0600))<0)

        {

            syslog(LOG_ERR, "open");

            exit(1);

        }

        write(fd, buf, len+1);

        close(fd);

        sleep(10);

    }

    closelog();

    exit(0);

}

读者可以尝试用普通用户的身份执行此程序,由于这里的 open 函数必须具有 root 权限,因此,syslog 就会将错误信息写入到“/var/log/messages”中,如下所示:

Jan 30 18:20:08  localhost demo_update[7996]: open

7.4  实验内容

7.4.1  编写多进程程序

1.实验目的

通过编写多进程程序,使读者熟练掌握 fork、exec、wait、waitpid 等函数的使用,进一步理解在 Linux 中多进程编程的步骤。


2.实验内容

该实验有 3 个进程,其中一个为父进程,其余两个是该父进程创建的子进程,其中一个子进程运行“ls -l”指令,另一个子进程在暂停 5s 之后异常退出,父进程并不阻塞自己,并等待子进程的退出信息,待收集到该信息,父进程就返回。

7.4.2  编写守护进程

1.实验目的

通过编写一个完整的守护进程,使读者掌握守护进程编写和调试的方法,并且进一步熟悉编写多进程程序。

2.实验内容

 

在该实验中,读者首先建立起一个守护进程,然后在该守护进程中新建一个子进程,该子进程暂停  10s,然后自动退出,并由守护进程收集子进程退出的消息。在这里,子进程和守护进程的退出消息都在“/var/log/messages”中输出。子进程退出后,守护进程循环暂停,其间隔时间为 10s。

思考与练习

查阅资料,明确 Linux 中进程处理和嵌入式 Linux 中对进程的处理有什么区别?


第8章 进程间通信

8.1       Linux 下进程间通信概述

在上一章中,读者已经知道了进程是一个程序的一次执行的过程。这里所说的进程一般是指运行在用户态的进程,而由于处于用户态的不同进程之间是彼此隔离的,就像处于不同城市的人们,它们必须通过某种方式来提供通信,例如人们现在广泛使用的手机等方式。本章就是讲述如何建立这些不同的通话方式,就像人们有多种通信方式一样。

Linux下的进程通信手段基本上是从   UNIX   平台上的进程通信手段继承而来的。而对UNIX 发展做出重大贡献的两大主力 AT&T 的贝尔实验室及 BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间的通信方面的侧重点有所不同。前者是对 UNIX 早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,其通信进程主要局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。而   Linux则把两者的优势都继承了下来,如图 8.1 所示。

    UNIX 进程间通信(IPC)方式包括管道、FIFO、信号。

 

图    进程间通信发展历程

    System V 进程间通信(IPC)包括 System V 消息队列、System V 信号灯、System V共享内存区。

    Posix   进程间通信(IPC)包括 Posix 消息队列、Posix 信号灯、Posix 共享内存区。

现在在 Linux 中使用较多的进程间通信方式主要有以下几种。

(1)管道(Pipe)及有名管道(named  pipe):管道可用于具有亲缘关系进程间的通信,有名管道,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

(2)信号(Signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知接受进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的。

(3)消息队列:消息队列是消息的链接表,包括 Posix 消息队列 systemV 消息队列。它克服了前两种通信方式中信息量有限的缺点,具有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。

(4)共享内存:可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块

内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。

(5)信号量:主要作为进程间以及同一进程不同线程之间的同步手段。

(6)套接字(Socket):这是一种更为一般的进程间通信机制,它可用于不同机器之间的进程间通信,应用非常广泛。

本章会详细介绍前 4 种进程通信方式,对第 5 种通信方式将会在第 10 章中单独介绍。

8.2  管道通信

8.2.1  管道概述

细心的读者可能会注意到本书在第 2 章中介绍“ps”的命令时提到过管道,当时指出了管道是 Linux 中很重要的一种通信方式,它是把一个程序的输出直接连接到另一个程序的输入,这里仍以第 2 章中的“ps –ef|grep ntp”为例,描述管道的通信过程,如图 8.2 所示。

 

管道是 Linux 中进程间通信的一种方式。这里所说的管道主要指无名管道,它具有如下

特点。

 它只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间)。

 它是一个半双工的通信模式,具有固定的读端和写端。

 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的   read、write   等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

8.2.2  管道创建与关闭

1.管道创建与关闭说明

管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符 fds[0]和 fds[1],其中 fds[0]固定用于读管道,而 fds[1]固定用于写管道,如图 8.3 所示,这样就构成了一个半双工的通道。

 

管道关闭时只需将这两个文件描述符关闭即可,可使用普通的close函数逐个关闭各个文件描述符。

注意  一个管道共享了多对文件描述符时,若将其中的一对读写文件描述符都删除,则该管道就失效。

2.管道创建函数

创建管道可以通过调用 pipe 来实现,下表 8.1 列出了 pipe 函数的语法要点。

3.管道创建实例

创建管道非常简单,只需调用函数 pipe 即可,如下所示:

/*pipe.c*/

#include <unistd.h>

#include <errno.h>

#include <stdio.h>

#include <stdlib.h>



int main()

{

      int pipe_fd[2];

/*创建一无名管道*/

      if(pipe(pipe_fd)<0)

      {

      printf("pipe create error\n");

      return 1;

      }

      else

    printf("pipe create success\n");

/*关闭管道描述符*/

close(pipe_fd[0]);

close(pipe_fd[1]);

}

 

 

程序运行后先成功创建一个无名管道,之后再将其关闭。

8.2.3  管道读写

1.管道读写说明

pipe 函数创建的管道两端处于一个进程中,由于管道是主要用于在不同进程间通信的,因此这在实际应用中没有太大意义。实际上,通常先是创建一个管道,再通过fork()函数创建一子进程,该子进程会继承父进程所创建的管道,这时,父子进程管道的文件描述符对应关系就如图 8.4 所示。

 

这时的关系看似非常复杂,实际上却已经给不同进程之间的读写创造了很好的条件。这时,父子进程分别拥有自己的读写的通道,为了实现父子进程之间的读写,只需把无关的读端或写端的文件描述符关闭即可。例如在图 8.5 中把父进程的写端 fd[1]和子进程的读端 fd[0]关闭。这时,父子进程之间就建立起了一条“子进程写入父进程读”的通道。

 

同样,也可以关闭父进程的 fd[0]和子进程的  fd[1],这样就可以建立一条“父进程写,子进程读”的通道。另外,父进程还可以创建多个子进程,各个子进程都继承了相应的 fd[0]和 fd[1],这时,只需要关闭相应端口就可以建立其各子进程之间的通道。

想一想 为什么无名管道只能建立具有亲缘关系的进程之间?

 

2.管道读写实例

在本例中,首先创建管道,之后父进程使用 fork 函数创建子进程,之后通过关闭父进程的读描述符和子进程的写描述符,建立起它们之间的管道通信。

/*pipe_rw.c*/

#include <unistd.h>

#include <sys/types.h>

#include <errno.h>

#include <stdio.h>

#include <stdlib.h>

int main()

{

      int pipe_fd[2];

      pid_t pid;

      char buf_r[100];

      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");

/*关闭子进程写描述符,并通过暂停2 秒确保父进程已关闭相应的读描述符*/

close(pipe_fd[1]);

    sleep(2);

/*子进程读取管道内容*/

    if((r_num=read(pipe_fd[0],buf_r,100))>0){ 

      printf("%d numbers read from the pipe is %s\n",r_num,buf_r);

    }

/*关闭子进程读描述符*/

    close(pipe_fd[0]);

    exit(0);

      }

else if(pid>0)

      {

/*/关闭父进程读描述符,并分两次向管道中写入Hello Pipe*/

close(pipe_fd[0]);

    if(write(pipe_fd[1],"Hello",5)!=-1)

      printf("parent write1 success!\n");

    if(write(pipe_fd[1]," Pipe",5)!=-1)

      printf("parent write2 success!\n");

/*关闭父进程写描述符*/

close(pipe_fd[1]);

    sleep(3);

/*收集子进程退出信息*/

waitpid(pid,NULL,0);

    exit(0);

      }

}

 

将该程序交叉编译,下载到开发板上的运行结果如下所示:

 

[root@(none) 1]# ./pipe_rw2

parent write1 success!

parent write2 success!



10 numbers read from the pipe is Hello Pipe

 

3.管道读写注意点

只有在管道的读端存在时向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的 SIFPIPE 信号(通常 Broken pipe 错误)。

 向管道中写入数据时,linux 将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。

 父子进程在运行时,它们的先后次序并不能保证,因此,在这里为了保证父进程已经关闭了读描述符,可在子进程中调用 sleep 函数。

8.2.4  标准流管道

1.标准流管道函数说明

与 Linux 中文件操作有基于文件流的标准 I/O 操作一样,管道的操作也支持基于文件流的模式。这种基于文件流的管道主要是用来创建一个连接到另一个进程的管道,这里的“另一个进程”也就是一个可以进行一定操作的可执行文件,例如,用户执行“cat  popen.c”或者自己编写的程序“hello”等。由于这一类操作很常用,因此标准流管道就将一系列的创建过程合并到一个函数 popen 中完成。它所完成的工作有以下几步。

 创建一个管道。

 fork 一个子进程。

 在父子进程中关闭不需要的文件描述符。

 执行 exec 函数族调用。

 执行函数中所指定的命令。

这个函数的使用可以大大减少代码的编写量,但同时也有一些不利之处,例如,它没有前面管道创建的函数灵活多样,并且用 popen 创建的管道必须使用标准 I/O 函数进行操作,但不能使用前面的 read、write 一类不带缓冲的 I/O 函数。

与之相对应,关闭用 popen 创建的流管道必须使用函数 pclose 来关闭该管道流。该函数关闭标准 I/O 流,并等待命令执行结束。

 

2.函数格式

popen 和 pclose 函数格式如表 8.2 和表 8.3 所示。

 

3.函数使用实例

在该实例中,使用 popen 来执行“ps  -ef”命令。可以看出,popen 函数的使用能够使程序变得短小精悍。

 

/*popen.c*/

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

#include <fcntl.h>

#define BUFSIZE 1000

int main()

{

      FILE *fp;

      char *cmd = "ps -ef";

      char buf[BUFSIZE];



/*调用popen 函数执行相应的命令*/

      if((fp=popen(cmd,"r"))==NULL)

    perror("popen");

      while((fgets(buf,BUFSIZE,fp))!=NULL)

    printf("%s",buf);

      pclose(fp);

      exit(0);

}


下面是该程序在目标板上的执行结果。

 

[root@(none) 1]# ./popen

  PID TTY          Uid        Size State Command

    1              root       1832   S   init

    2              root                   0   S   [keventd]

    3              root                   0   S   [ksoftirqd_CPU0]

    4              root                   0   S   [kswapd]

    5              root                   0   S   [bdflush]

    6              root                  0   S   [kupdated]

    7              root                  0   S   [mtdblockd]

    8              root                  0   S   [khubd]

   35              root       2104   S   /bin/bash /usr/etc/rc.local

   36              root       2324   S   /bin/bash

   41              root       1364   S   /sbin/inetd

   53              root      14260   S   /Qtopia/qtopia-free-1.7.0/bin/qpe -qws

   54              root      11672   S   quicklauncher

   55              root                   0   S   [usb-storage-0]

   56              root                   0   S   [scsi_eh_0]

   74              root       1284   S   ./popen

75              root       1836   S   sh -c ps -ef

   76              root       2020   R   ps   –ef

 

8.2.5  FIFO

1.有名管道说明

前面介绍的管道是无名管道,它只能用于具有亲缘关系的进程之间,这就大大地限制了管道的使用。有名管道的出现突破了这种限制,它可以使互不相关的两个进程实现彼此通信。

管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便。不过值得注意的是,FIFO是严格地遵循先进先出规则的,对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如 lseek()等文件定位操作。

有名管道的创建可以使用函数mkfifo(),该函数类似文件中的open()操作,可以指定管道的路径和打开的模式。

小知识 用户还可以在命令行使用“mknod   管道名 p”来创建有名管道。

在创建管道成功之后,就可以使用 open、read、write 这些函数了。与普通文件的开发设置一样,对于为读而打开的管道可在 open 中设置 O_RDONLY,对于为写而打开的管道可在open 中设置 O_WRONLY,在这里与普通文件不同的是阻塞问题。由于普通文件的读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能,这里的非阻塞标志可以在 open 函数中设定为 O_NONBLOCK。下面分别对阻塞打开和非阻塞打开的读写进行一定的讨论。

对于读进程

 若该管道是阻塞打开,且当前 FIFO 内没有数据,则对读进程而言将一直阻塞直到有数据写入。

 若该管道是非阻塞打开,则不论 FIFO 内是否有数据,读进程都会立即执行读操作。

对于写进程

 若该管道是阻塞打开,则写进程而言将一直阻塞直到有读进程读出数据。

 若该管道是非阻塞打开,则当前 FIFO 内没有读操作,写进程都会立即执行读操作。


2.mkfifo 函数格式

表 8.4 列出了 mkfifo 函数的语法要点。

 

表 8.5 再对 FIFO 相关的出错信息做一归纳,以方便用户差错。

 

3.使用实例

下面的实例包含了两个程序,一个用于读管道,另一个用于写管道。其中在读管道的程序里创建管道,并且作为 main 函数里的参数由用户输入要写入的内容。读管道读出了用户写入管道的内容,这两个函数用的是非阻塞读写管道。

/*fifo_write.c*/

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#define FIFO "/tmp/myfifo"



main(int argc,char** argv)

/*参数为即将写入的字节数*/

{

int fd;

char w_buf[100];

int nwrite;

if(argc<=1)

    {

printf("Please send something\n");

exit(1);

         }

strcpy(w_buf,argv[1]);

/*打开FIFO 管道,并设置非阻塞标志*/

fd=open(FIFO,O_WRONLY|O_NONBLOCK,0);

if(fd==-1)

{

printf("open error; no reading process\n");

exit(1);

         }

/*向管道中写入字符串*/

      if((nwrite=write(fd,w_buf,100))==-1)

      {

      if(errno==EAGAIN)

      printf("The FIFO has not been read yet.Please try later\n");

      }

      else

      printf("write %s to the FIFO\n",w_buf);

}



/*fifl_read.c*/

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#define FIFO "/tmp/myfifo"



main(int argc,char** argv)

{

char buf_r[100];

int  fd;

int  nread;

/*创建有名管道并设置相应的权限*/

if((mkfifo(FIFO,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))

    printf("cannot create fifoserver\n");

printf("Preparing for reading bytes...\n");

/*打开有名管道,并设置非阻塞标志*/

fd=open(FIFO,O_RDONLY|O_NONBLOCK,0);

      if(fd==-1)

      {

    perror("open");

    exit(1);

      }

      while(1)

      {

    memset(buf_r,0,sizeof(buf_r));

    if((nread=read(fd,buf_r,100))==  -1)

{

      if(errno==EAGAIN)

        printf("no data yet\n");

    }

    printf("read %s from FIFO\n",buf_r);

    sleep(1);

      }  

      pause();

      unlink(FIFO);

}

8.3  信号通信

8.3.1  信号概述

信号是 UNIX 中所使用的进程通信的一种最古老的方法。它是在软件层次上对中断机制的一种模拟,是一种异步通信方式。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

细心的读者是否还记得,在第 2 章 kill 命令中曾讲解到“l”选项,这个选项可以列出该系统所支持的所有信号列表。在笔者的系统中,信号值在32之前的则有不同的名称,而信号值在 32 以后的都是用“ SIGRTMIN”或“SIGRTMAX”开头的,这就是两类典型的信号。前者是从 UNIX 系统中继承下来的信号,为不可靠信号(也称为非实时信号);

后者是为了解决前面“不可靠信号”的问题而进行了更改和扩充的信号,称为“可靠信号”(也称为实时信号)。那么为什么之前的信号不可靠呢?这里首先要介绍一下信号的生命周期。

一个完整的信号生命周期可以分为 3 个重要阶段,这 3 个阶段由 4 个重要事件来刻画的:信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数,如图8.6 所示。相邻两个事件的时间间隔构成信号生命周期的一个阶段。要注意这里的信号处理有多种方式,一般是由内核完成的,当然也可以由用户进程来完成,故在此没有明确画出。

一个不可靠信号的处理过程是这样的:如果发现该信号已经在进程中注册,那么就忽略该信号。因此,若前一个信号还未注销又产生了相同的信号就会产生信号丢失。而当可靠信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此信号就不会丢失。所有可靠信号都支持排队,而不可靠信号则都不支持排队。

注意

这里信号的产生、注册、注销等是指信号的内部实现机制,而不是信号的函数实现。因此,信号注册与否,与本节后面讲到的发送信号函数(如 kill()等)以及信号安装函数(如 signal()等)无关,只与信号值有关。

用户进程对信号的响应可以有 3 种方式。

忽略信号,即对信号不做任何处理,但是有两个信号不能忽略,即  SIGKILL及SIGSTOP

捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数。

执行缺省操作,Linux 对每种信号都规定了默认操作。

Linux 中的大多数信号是提供给内核的,表 8.6 列出了 Linux 中最为常见信号的含义及其默认操作。

8.3.2  信号发送与捕捉

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


1.kill()和

(1)函数说明

kill 函数同读者熟知的 kill 系统命令一样,可以发送信号给进程或进程组(实际上,kill系统命令只是 kill 函数的一个用户接口)。这里要注意的是,它不仅可以中止进程(实际上发出 SIGKILL 信号),也可以向进程发送其他信号。

与 kill 函数所不同的是,raise 函数允许进程向自身发送信号。

(2)函数格式

表 8.7 列出了 kill 函数的语法要点。


表 8.8 列出了 raise 函数的语法要点。


(3)函数实例

下面这个示例首先使用 fork 创建了一个子进程,接着为了保证子进程不在父进程调用 kill之前退出,在子进程中使用 raise 函数向子进程发送 SIGSTOP 信号,使子进程暂停。接下来再在父进程中调用 kill 向子进程发送信号,在该示例中使用的是 SIGKILL,读者可以使用其他信号进行练习。

/*kill.c*/

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <sys/types.h>

#include <sys/wait.h>

int main()

{

      pid_t pid;

      int ret;

/*创建一子进程*/

      if((pid=fork())<0){

    perror("fork");

    exit(1);

      }

      if(pid == 0){

/*在子进程中使用raise 函数发出SIGSTOP 信号*/

raise(SIGSTOP);

    exit(0);

      }

      else{

/*在父进程中收集子进程发出的信号,并调用kill 函数进行相应的操作*/

    printf("pid=%d\n",pid);

    if((waitpid(pid,NULL,WNOHANG))==0){

      if((ret=kill(pid,SIGKILL))==0)

        printf("kill %d\n",pid);

      else{

  perror("kill");

      }

    }

      }

}

该程序运行结果如下所示:


[root@(none) tmp]# ./kill             

pid=78

kill 78


2.alarm()和

 

(1)函数说明

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

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

 

(2)函数格式

表 8.9 列出了 alarm 函数的语法要点。

表 8.10 列出了 pause 函数的语法要点。


(3)函数实例

该实例实际上已完成了一个简单的 sleep 函数的功能,由于 SIGALARM 默认的系统动作为终止该进程,因此在程序调用 pause 之后,程序就终止了。如下所示:

/*alarm.c*/

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>



int main()

{

      int ret;

/*调用alarm 定时器函数*/

ret=alarm(5);

      pause();

      printf("I have been waken up.\n",ret);/*未执行*/

}

[root@(none) tmp]#./alarm

Alarm clock


想一想 用这种形式实现的 sleep 功能有什么问题?

8.3.3  信号的处理

在了解了信号的产生与捕获之后,接下来就要对信号进行具体的操作了。从前面的信号概述中读者也可以看到,特定的信号是与一定的进程相联系的。也就是说,一个进程可以决定在该进程中需要对哪些信号进行什么样的处理。例如,一个进程可以选择忽略某些信号而只处理其他一些信号,另外,一个进程还可以选择如何处理信号。总之,这些都是与特定的进程相联系的。因此,首先就要建立其信号与进程之间的对应关系,这就是信号的处理。

注意

请读者区分信号的注册与信号的处理之间的差别,前者信号是主动方,而后者进程是主动方。信号的注册是在进程选择了特定信号处理之后特定信号的主动行为。

信号处理的主要方法有两种,一种是使用简单的 signal 函数,另一种是使用信号集函数组。下面分别介绍这两种处理方式。

1.signal()

 

(1)函数说明

使用 signal 函数处理时,只需把要处理的信号和处理函数列出即可。它主要是用于前 32种非实时信号的处理,不支持信号传递信息,但是由于使用简单、易于理解,因此也受到很多程序员的欢迎。

 

(2)函数格式

signal 函数的语法要点如表 8.11 所示。

8.11 signal 函数语法要点

这里需要对这个函数原型进行说明。这个函数原型非常复杂。可先用如下的    typedef    进行替换说明:

typedef void sign(int);

sign *signal(int, handler *);

可见,首先该函数原型整体指向一个无返回值带一个整型参数的函数指针,也就是信号的原始配置函数。接着该原型又带有两个参数,其中的第二个参数可以是用户自定义的信号处理函数的函数指针。

 

(3)使用实例

该示例表明了如何使用 signal 函数捕捉相应信号,并做出给定的处理。这里,my_func就是信号处理的函数指针。读者还可以将其改为 SIG_IGN 或 SIG_DFL 查看运行结果。

/*mysignal.c*/

#include <signal.h>

#include <stdio.h>

#include <stdlib.h>

/*自定义信号处理函数*/

void my_func(int sign_no)

{

      if(sign_no==SIGINT)

    printf("I have get SIGINT\n");

    else if(sign_no==SIGQUIT)

    printf("I have get SIGQUIT\n");

}

int main()

{

      printf("Waiting for signal SIGINT or SIGQUIT \n ");

/*发出相应的信号,并跳转到信号处理函数处*/

signal(SIGINT, my_func);        /* ctrl+c */

signal(SIGQUIT, my_func);        /* ctrl+\ */

      pause();

      exit(0);

}

[root@130 test]# ./mysignal

Waiting for signal SIGINT or SIGQUIT

I have get SIGINT

[root@130 test]# ./mysignal

Waiting for signal SIGINT or SIGQUIT

I have get SIGQUIT

 

2.信号集函数组

(1)函数说明

使用信号集函数组处理信号时涉及一系列的函数,这些函数按照调用的先后次序可分为以下几大功能模块:创建信号集合、登记信号处理器以及检测信号。其中,创建信号集合主要用于创建用户感兴趣的信号,其函数包括以下几个。

    sigemptyset:初始化信号集合为空。

    sigfillset:初始化信号集合为所有信号的集合。

    sigaddset:将指定信号加入到信号集合中去。

    sigdelset:将指定信号从信号集中删去。

    sigismember:查询指定信号是否在信号集合之中。

登记信号处理器主要用于决定进程如何处理信号。这里要注意的是,信号集里的信号并不是真正可以处理的信号,只有当信号的状态处于非阻塞状态时才真正起作用。因此,首先就要判断出当前阻塞能不能传递给该信号的信号集。这里首先使用 sigprocmask 函数判断检测或更改信号屏蔽字,然后使用 sigaction函数用于改变进程接收到特定信号之后的行为。

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

 

(2)函数格式

首先介绍创建信号集合的函数格式,表 8.12 列举了这一组函数的语法要点。

表 8.13 列举了 sigprocmask 的语法要点。

 

此处,若 set 是一个非空指针,则参数 how 表示函数的操作方式;若 how 为空,则表示忽略此操作。

表 8.14 列举了 sigaction 的语法要点。

 

这里要说明的是 sigaction 函数中第 2 个和第 3 个参数用到的sigaction 结构。这是一个看似非常复杂的结构,希望读者能够慢慢阅读此段内容。

首先给出了 sigaction 的定义,如下所示:

 

struct sigaction {

void (*sa_handler)(int signo);

sigset_t sa_mask;

int sa_flags;

void (*sa_restore)(void);

}

 

sa_handler是一个函数指针,指定信号关联函数,这里除可以是用户自定义的处理函数外,还可以为 SIG_DFL(采用缺省的处理方式)或 SIG_IGN(忽略信号)。它的处理函数只有一个参数,即信号值。

sa_mask是一个信号集,它可以指定在信号处理程序执行过程中哪些信号应当被阻塞,在调用信号捕获函数之前,该信号集要加入到信号的信号屏蔽字中。

sa_flags中包含了许多标志位,是对信号进行处理的各个选择项。它的常见可选值如下表 8.15 所示。

 

 

最后,表 8.16 列举了 sigpending 函数的语法要点。

 

总之,在处理信号时,一般遵循如图 8.7 所示的操作流程。

 

             图    信号操作一般处理流程

 

(3)使用实例

该实例首先把 SIGQUIT、SIGINT 两个信号加入信号集,然后将该信号集设为阻塞状态,并在该状态下使程序暂停 5 秒。接下来再将信号集设置为非阻塞状态,再对这两个信号分别操作,其中 SIGQUIT 执行默认操作,而 SIGINT 执行用户自定义函数的操作。源代码如下所示:

/*sigaction.c*/

#include <sys/types.h>

#include <unistd.h>

#include <signal.h>

#include <stdio.h>

#include <stdlib.h>



/*自定义的信号处理函数*/

void my_func(int signum)

{

      printf("If you want to quit,please try SIGQUIT\n");

}

int main()

{

      sigset_t set,pendset;

      struct sigaction action1,action2;

/*初始化信号集为空*/

      if(sigemptyset(&set)<0)

    perror("sigemptyset");

/*将相应的信号加入信号集*/

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

    perror("sigaddset");

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

    perror("sigaddset");

/*设置信号集屏蔽字*/

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

    perror("sigprocmask");

      else

      {

    printf("blocked\n");

    sleep(5);

      }

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

    perror("sigprocmask");

      else

    printf("unblock\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(SIGTERM,&action2,NULL);

    }

      }

}

 

该程序的运行结果如下所示,可以看见,在信号处于阻塞状态时,所发出的信号对进程不起作用。读者需等待 5 秒,在信号解除阻塞状态之后,用户发出的信号才能正常运行。这里 SIGINT 已按照用户自定义的函数运行。

 

[root@(none) tmp]# ./sigaction

blocked

unblock

If you want to quit,please try SIGQUIT

Quit

8.4  共享内存

8.4.1  共享内存概述

可以说,共享内存是一种最为高效的进程间通信方式。因为进程可以直接读写内存,不需要任何数据的拷贝。为了在多个进程间交换信息,内核专门留出了一块内存区。这段内存区可以由需要访问的进程将其映射到自己的私有地址空间。因此,进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高了效率。当然,由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。其原理示意图如图 8.8所示。

8.4.2  共享内存实现

1.函数说明

共享内存的实现分为两个步骤,第一步是创建共享内存,这里用到的函数是 shmget,也就是从内存中获得一段共享内存区域。第二步映射共享内存,也就是把这段创建的共享内存映射到具体的进程空间去,这里使用的函数是 shmat。到这里,就可以使用这段共享内存了,也就是可以使用不带缓冲的 I/O 读写命令对其进行操作。除此之外,当然还有撤销映射的操作,其函数为 shmdt。这里就主要介绍这 3 个函数。

 

2.函数格式

表 8.17 列举了 shmget 函数的语法要点。

所需头文件

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

函数原型

int shmget(key_t key,int size,int shmflg)

函数传入值

key:共享内存的键值,多个进程可以通过它访问同一个内存,其中有个特殊值IPC_PRIVATE,它用于创建当前进程的私有共享内存

size:共享内存区大小

shmflg:同open()函数的权限位,也可以用八进制表示法

函数返回值

成功:共享内存段标识符

出错:-1

 

表 8.18 列举了 shmat函数的语法要点。

表 8.19 列举了 shmdt 函数的语法要点。

 

3.使用实例

该实例说明了如何使用基本的共享内存函数,首先是创建一个共享内存区,之后将其映射到本进程中,最后再解除这种映射关系。这里要介绍的一个命令是 ipcs,这是用于报告进程间通信机制状态的命令。它可以查看共享内存、消息队列等各种进程间通信机制的情况,这里使用了 system 函数用于调用 shell 命令“ipcs”。程序源代码如下所示:

/*shmadd.c*/

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <stdio.h>

#include <stdlib.h>



#define BUFSZ 2048

int main()

{

      int shmid;

      char *shmadd;

/*创建共享内存*/

if((shmid=shmget(IPC_PRIVATE,BUFSZ,0666))<0){

    perror("shmget");

    exit(1);

      }

      else

    printf("created shared-memory: %d\n",shmid);

    system("ipcs -m");    /* 示范了如何在C程序中调用系统命令的用法

/*映射共享内存*/

      if((shmadd=shmat(shmid,0,0))<(char *)0){

    perror("shmat");

    exit(1);

      }

      else

    printf("attached shared-memory\n");

/*显示系统内存情况*/

    system("ipcs -m");

/*删除共享内存*/

      if((shmdt(shmadd))<0){

    perror("shmdt");

    exit(1);

      }

      else

    printf("deleted shared-memory\n");

      system("ipcs -m");

      exit(0);

}


下面是运行结果。从该结果可以看出,nattch  的值随着共享内存状态的变化而变化,共享内存的值根据不同的系统会有所不同。

 

created shared-memory: 229383



------ Shared Memory Segments --------

key         shmid       owner      perms      bytes      nattch     status      

0x00000000 229383     root      666        2048       0                       

 

 

attached shared-memory


------ Shared Memory Segments --------

key        shmid      owner      perms      bytes      nattch     status      

0x00000000 229383     root      666        2048       1                       

 


deleted shared-memory



------ Shared Memory Segments --------

key        shmid      owner      perms      bytes      nattch     status      

0x00000000 229383     root      666        2048       0


8.5  消息队列

8.5.1  消息队列概述

顾名思义,消息队列就是一个消息的列表。用户可以从消息队列种添加消息、读取消息等。从这点上看,消息队列具有一定的FIFO 的特性,但是它可以实现消息的随机查询,比FIFO 具有更大的优势。同时,这些消息又是存在于内核中的,由“队列 ID”来标识。

8.5.2  消息队列实现

1.函数说明

消息队列的实现包括创建或打开消息队列、添加消息、读取消息和控制消息队列这四种操作。其中创建或打开消息队列使用的函数是 msgget,这里创建的消息队列的数量会受到系统消息队列数量的限制;添加消息使用的函数是 msgsnd 函数,它把消息添加到已打开的消息队列末尾;读取消息使用的函数是 msgrcv,它把消息从消息队列中取走,与 FIFO 不同的是,这里可以指定取走某一条消息;最后控制消息队列使用的函数是 msgctl,它可以完成多项功能。

 

2.函数格式

表 8.20 列举了 msgget 函数的语法要点。

表 8.21 列举了 msgsnd 函数的语法要点。


表 8.22 列举了 msgrcv 函数的语法要点。



表 8.23 列举了 msgrcv 函数的语法要点。

表 8.24 列举了 msgctl 函数的语法要点。


3.使用实例

这个实例体现了如何使用消息队列进行进程间通信,包括消息队列的创建、消息发送与读取、消息队列的撤销等多种操作。注意这里使用了函数 fotk,它可以根据不同的路径和关键表示产生标准的 key。程序源代码如下所示:

/*msg.c*/

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#define BUFSZ 512

struct message{

long msg_type;


char msg_text[BUFSZ];

};

int main()

{

      int qid;

      key_t key;

      int len;

      struct message msg;

/*根据不同的路径和关键表示产生标准的key*/

      if((key=ftok(".",'a'))==-1){

    perror("ftok");

    exit(1);

      }

/*创建消息队列*/

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

    perror("msgget");

    exit(1);

      }

      printf("opened queue %d\n",qid);

      puts("Please enter the message to queue:");

      if((fgets((&msg)->msg_text,BUFSZ,stdin))==NULL){

    puts("no message");

    exit(1);

      }

      msg.msg_type = getpid();

      len = strlen(msg.msg_text);

/*添加消息到消息队列*/

      if((msgsnd(qid,&msg,len,0))<0){

perror("message posted");

exit(1);

  }

/*读取消息队列*/

      if(msgrcv(qid,&msg,BUFSZ,0,0)<0){

    perror("msgrcv");

    exit(1);

      }

      printf("message is:%s\n",(&msg)->msg_text);

/*从系统内核中移走消息队列。*/

      if((msgctl(qid,IPC_RMID,NULL))<0){

    perror("msgctl");

    exit(1);

      }

      exit(0);

}

以下是程序的运行结果。

[root@(none) tmp]# ./msg   

opened queue 262146

Please enter the message to queue:

hello

message is:hello

8.6  实验内容

8.6.1  管道通信实验

1.实验目的

通过编写有名管道多路通信实验,读者可进一步熟练掌握管道的创建、读写等操作,同时,也复习使用 select 函数实现管道的通信。

2.实验内容

在该实验中,要求创建两个管道,首先读出管道一中的数据,再把从管道一中读入的数据写入到管道二中去。这里的 select 函数采用阻塞形式,也就是首先在程序中实现将数据写入管道一,并通过 select 函数实现将管道一的数据读出,并写入管道二,接着该程序一直等待用户输入管道一的数据并将其即时读出。

8.6.2  共享内存实验

1.实验目的

通过编写共享内存实验,读者就可以进一步了解共享内存的具体步骤,同时也进一步加深了对共享内存的理解。由于共享内存涉及同步机制,关于这方面的知识本书现在还没有涉及,因此,现在只在一个进程中对共享内存进行操作。


2.实验内容

该实验要求利用共享内存实现文件的打开、读写操作。

思考与练习

1.通过自定义信号完成进程间的通信。

2.编写一个简单的管道程序实现文件传输。


第9章 多线程编程

9.1  Linux 下线程概述

9.1.1  线程概述

前面已经提到,进程是系统中程序执行和资源分配的基本单位。每个进程都拥有自己的数据段、代码段和堆栈段,这就造成了进程在进行切换等操作时都需要有比较负责的上下文切换等动作。为了进一步减少处理机的空转时间支持多处理器和减少上下文切换开销,进程在演化中出现了另一个概念——线程。它是一个进程内的基本调度单位,也可以称为轻量级进程。线程是在共享内存空间中并发的多道执行路径,它们共享一个进程的资源,如文件描述和信号处理。因此,大大减少了上下文切换的开销。

同进程一样,线程也将相关的变量值放在线程控制表内。一个进程可以有多个线程,也就是有多个线程控制表及堆栈寄存器,但却共享一个用户地址空间。要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响,因此,多线程中的同步就是非常重要的问题了。在多线程系统中,进程与线程的关系如表 9.1 所示。

9.1.2  线程分类

线程按照其调度者可以分为用户级线程和核心级线程两种。

(1)用户级线程

用户级线程主要解决的是上下文切换的问题,它的调度算法和调度过程全部由用户自行选择决定,在运行时不需要特定的内核支持。在这里,操作系统往往会提供一个用户空间的线程库,该线程库提供了线程的创建、调度、撤销等功能,而内核仍然仅对进程进行管理。

如果一个进程中的某一个线程调用了一个阻塞的系统调用,那么该进程包括该进程中的其他所有线程也同时被阻塞。这种用户级线程的主要缺点是在一个进程中的多个线程的调度中无法发挥多处理器的优势。

(2)核心级线程

这种线程允许不同进程中的线程按照同一相对优先调度方法进行调度,这样就可以发挥多处理器的并发优势。

现在大多数系统都采用用户级线程与核心级线程并存的方法。一个用户级线程可以对应一个或几个核心级线程,也就是“一对一”或“多对一”模型。这样既可满足多处理机系统的需要,也可以最大限度地减少调度开销。

9.1.3  Linux 线程技术的发展

在 Linux 中,线程技术也经过了一代代的发展过程。

在 Linux2.2 内核中,并不存在真正意义上的线程。当时 Linux 中常用的线程 pthread 实际上是通过进程来模拟的,也就是说 Linux 中的线程也是通过 fork 创建的“轻”进程,并且线程的个数也很有限,最多只能有 4096 个进程/线程同时运行。

Linux2.4 内核消除了这个线程个数的限制,并且允许在系统运行中动态地调整进程数上限。

当时采用的是 LinuxThread 线程库,它对应的线程模型是“一对一”线程模型,也就是一个用户级线程对应一个内核线程,而线程之间的管理在内核外函数库中实现。这种线程模型得到了广泛应用。但是,LinuxThread 也由于 Linux 内核的限制以及实现难度等原因,并不是完全与 POSIX兼容。另外,它的进程 ID、信号处理、线程总数、同步等各方面都还有诸多的问题。

为了解决以上问题,在 Linux2.6 内核中,进程调度通过重新编写,删除了以前版本中效率不高的算法。内核线程框架也被重新编写,开始使用 NPTL(Native POSIX Thread Library)线程库。这个线程库有以下几点设计目标:POSIX 兼容性、多处理器结构的应用、低启动开销、低链接开销、与 LinuxThreads 应用的二进制兼容性、软硬件的可扩展能力、与 C++集成等。这一切都使得Linux2.6内核的线程机制更加完备,能够更好地完成其设计目标。与LinuxThreads不同,NPTL 没有使用管理线程,核心线程的管理直接放在核内进行,这也带了性能的优化。由于 NPTL 仍然采用 1∶1 的线程模型,NPTL 仍然不是 POSIX 完全兼容的,但就性能而言相对 LinuxThreads 已经有很大程度上的改进。

9.2  Linux 线程实现

9.2.1  线程基本操作

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

1.线程创建和退出

(1)函数说明

创建线程实际上就是确定调用该线程函数的入口点,这里通常使用的函数是 pthread_create。在线程创建以后,就开始运行相关的线程函数,在该函数运行完之后,该线程也就退出了,这也是线程退出一种方法。另一种退出线程的方法是使用函数  pthread_exit,这是线程的主动行为。

这里要注意的是,在使用线程函数时,不能随意使用 exit 退出函数进行出错处理,由于 exit 的作用是使调用进程终止,往往一个进程包含多个线程,因此,在使用 exit 之后,该进程中的所有线程都终止了。因此,在线程中就可以使用 pthread_exit 来代替进程中的 exit。

由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放。正如进程之间可以用wait()系统调用来同步终止并释放资源一样,线程之间也有类似机制,那就是 pthread_join()函数。pthread_join 可以用于将当前线程挂起,等待线程的结束。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源就被收回。

(2)函数格式

表 9.1 列出了 pthread_create 函数的语法要点。

 

表 9.2 列出了 pthread_exit 函数的语法要点。

 

表 9.3 列出了 pthread_join 函数的语法要点。

 

 

(3)函数使用

以下实例中创建了两个线程,其中第一个线程是在程序运行到中途时调用pthread_exit函数退出,第二个线程正常运行退出。在主线程中收集这两个线程的退出信息,并释放资源。从这个实例中可以看出,这两个线程是并发运行的。

/*thread.c*/

#include <stdio.h>

#include <pthread.h>


/*线程一*/

void thread1(void)

{

    int i=0;

    for(i=0;i<6;i++)

    {

        printf("This is a pthread1.\n");

        if(i==2)

            pthread_exit(0);


        sleep(1);

    }

}


/*线程二*/

void thread2(void)

{

    int i;

    for(i=0;i<3;i++)

        printf("This is a pthread2.\n");

    pthread_exit(0);

}


int main(void)

{

    pthread_t id1,id2;

    int i,ret;


    /*创建线程一*/

    ret=pthread_create(&id1,NULL,(void *) thread1,NULL);

    if(ret!=0)

    {

        printf ("Create pthread error!\n");

        exit (1);

    }


    /*创建线程二*/

    ret=pthread_create(&id2,NULL,(void *) thread2,NULL);

    if(ret!=0)

    {

        printf ("Create pthread error!\n");

        exit (1);

    }


    /*等待线程结束*/

    pthread_join(id1,NULL);

    pthread_join(id2,NULL);

    exit (0);

}

以下是程序运行结果:

[root@(none) tmp]# ./thread

This is a pthread1.

This is a pthread2.

This is a pthread2.

This is a pthread2.

This is a pthread1.

This is a pthread1.

 

2.修改线程属性

(1)函数说明

读者是否还记得 pthread_create 函数的第二个参数——线程的属性。在上一个实例中,将该值设为 NULL,也就是采用默认属性,线程的多项属性都是可以更改的。这些属性主要包括绑定属性、分离属性、堆栈地址、堆栈大小、优先级。其中系统默认的属性为非绑定、非分离、缺省1M 的堆栈、与父进程同样级别的优先级。下面首先对绑定属性和分离属性的基本概念进行讲解。

 绑定属性

前面已经提到,Linux  中采用“一对一”的线程机制,也就是一个用户线程对应一个内核线程。绑定属性就是指一个用户线程固定地分配给一个内核线程,因为 CPU 时间片的调度是面向内核线程(也就是轻量级进程)的,因此具有绑定属性的线程可以保证在需要的时候总有一个内核线程与之对应。而与之相对的非绑定属性就是指用户线程和内核线程的关系不是始终固定的,而是由系统来控制分配的。

 分离属性

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

这些属性的设置都是通过一定的函数来完成的,通常首先调用 pthread_attr_init 函数进行初始化,之后再调用相应的属性设置函数。设置绑定属性的函数为 pthread_attr_setscope,设置 线 程 分 离 属 性 的 函 数 为  pthread_attr_setdetachstate , 设 置 线 程 优 先 级 的 相 关 函 数 为pthread_attr_getschedparam(获取线程优先级)和pthread_attr_setschedparam(设置线程优先级)。在设置完这些属性后,就可以调用 pthread_create 函数来创建线程了。

(2)函数格式

表 9.4 列出了 pthread_attr_init 函数的语法要点。

 

 

表 9.5 列出了 pthread_attr_setscope 函数的语法要点。

 

表 9.6 列出了 pthread_attr_setdetachstate 函数的语法要点。

 

 

表 9.7 列出了 pthread_attr_getschedparam 函数的语法要点。

 

表 9.8 列出了 pthread_attr_setschedparam 函数的语法要点。

 

3.使用实例

该实例将上一节中的第一个线程设置为分离属性,并将第二个线程设置为始终运行状态,这样就可以在第二个线程运行过程中查看内存值的变化。

其源代码如下所示:

 

/*pthread.c*/

#include <stdio.h>

#include <pthread.h>

#include <time.h>


/*线程一*/

void thread1(void)

{

    int i=0;


    for(i=0;i<6;i++)

    {

        printf("This is a pthread1.\n");


        if(i==2)

            pthread_exit(0);


        sleep(1);

    }

}



/*线程二*/

void thread2(void)

{

    int i;


    while(1)

    {

        for(i=0;i<3;i++)

            printf("This is a pthread2.\n");


        sleep(1);

    }

    pthread_exit(0);

}


int main(void)

{

    pthread_t id1,id2;

    int i,ret;

    pthread_attr_t attr;


    /*初始化线程*/

    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 ("Create pthread error!\n");

        exit (1);

    }


    ret=pthread_create(&id2,NULL,(void *) thread2,NULL);

    if(ret!=0)

    {

        printf ("Create pthread error!\n");

        exit (1);

    }

    pthread_join(id2,NULL);

    return (0);

}

接下来可以在线程一运行前后使用“free”命令查看内存使用情况。以下是运行结果:

 

[root@(none) tmp]# ./thread3

This is a pthread1.

This is a pthread2.

This is a pthread2.

This is a pthread2.

This is a pthread1.

This is a pthread2.

This is a pthread2.

This is a pthread2.

This is a pthread1.

This is a pthread2.

[root@130 test]# free

                total       used       free     shared    buffers     cached

Mem:          1028428      570212     458216    48       204292      93196

-/+ buffers/cache:  272724     755704

Swap: 1020116        0      1020116

[root@130 test]# free

                total       used       free     shared    buffers     cached

Mem:          1028428    570220     458208       48     204296      93196

-/+ buffers/cache:     272728     755700

Swap:         1020116          0    1020116

[root@130 test]# free

               total       used       free     shared    buffers     cached

Mem:          1028428     570212     458216      48     204296      93196

-/+ buffers/cache:   272720     755708

Swap:        1020116          0    1020116

 

 

可以看到,线程一在运行结束后就收回了系统资源,释放了内存。

9.2.2  线程访问控制

由于线程共享进程的资源和地址空间,因此在对这些资源进行操作时,必须考虑到线程间资源访问的惟一性问题,这里主要介绍 POSIX 中线程同步的方法,主要有互斥锁和信号量的方式。

1.mutex 互斥锁线程控制

(1)函数说明

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

互斥锁的操作主要包括以下几个步骤。

互斥锁初始化:pthread_mutex_init

 互斥锁上锁:pthread_mutex_lock

 互斥锁判断上锁:pthread_mutex_trylock

 互斥锁接锁:pthread_mutex_unlock

 消除互斥锁:pthread_mutex_destroy

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

(2)函数格式

表 9.9 列出了 pthread_mutex_init 函数的语法要点。


表 9.10 列出了 pthread_mutex_lock 等函数的语法要点。


(3)使用实例

该实例使用互斥锁来实现对变量 lock_var 的加一、打印操作。


/*mutex.c*/

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <pthread.h>

#include <errno.h>

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("pthread cread1");

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

    if(ret!=0)

        perror("pthread cread2");

    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("pthread_mutex_lock");

        }

        else

            printf("pthread1:pthread1 lock the variable\n");

        for(i=0;i<2;i++)

        {

            sleep(1);

            lock_var++;

        }

        /*互斥锁接锁*/

        if(pthread_mutex_unlock(&mutex)!=0)

        {

            perror("pthread_mutex_unlock");

        }

        else

            printf("pthread1:pthread1 unlock the variable\n");

  sleep(1);

    }

}

void pthread2(void *arg)

{

    int nolock=0;

    int ret;

    while(time(NULL) < end_time)

    {

        /*测试互斥锁*/

        ret=pthread_mutex_trylock(&mutex);


        if(ret==EBUSY)

            printf("pthread2:the variable is locked by pthread1\n");

        else

        {

            if(ret!=0)

            {

                perror("pthread_mutex_trylock");

                exit(1);

            }

            else

                printf("pthread2:pthread2  got  lock.The  variable  is %d\n",lock_var);


            /*互斥锁解锁*/

            if(pthread_mutex_unlock(&mutex)!=0)

            {

                perror("pthread_mutex_unlock");

            }

            else

                printf("pthread2:pthread2 unlock the variable\n");

        }

        sleep(3);

    }

}

该实例的运行结果如下所示:

 

[root@(none) tmp]# ./mutex2

pthread1:pthread1 lock the variable

pthread2:the variable is locked by pthread1

pthread1:pthread1 unlock the variable

pthread:pthread2 got lock.The variable is 2

pthread2:pthread2 unlock the variable

pthread1:pthread1 lock the variable

pthread1:pthread1 unlock the variable

pthread2:pthread2 got lock.The variable is 4

pthread2:pthread2 unlock the variable

pthread1:pthread1 lock the variable

 

2.信号量线程控制

(1)信号量说明

信号量也就是操作系统中所用到的PV 原语,它广泛用于进程或线程间的同步与互斥。信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。这里先来简单复习一下 PV 原语的工作原理。

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

PV 原语主要用于进程或线程间的同步和互斥这两种典型情况。若用于互斥,几个进程(或线程)往往只设置一个信号量 sem,它们的操作流程如图 9.2 所示。

当信号量用于同步操作时,往往会设置多个信号量,并安排不同的初始值来实现它们之间的顺序执行,它们的操作流程如图 9.3 所示。

(2)函数说明

 Linux 实现了 POSIX 的无名信号量,主要用于线程间的互斥同步。这里主要介绍几个常见函数。

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

    sem_wait 和 sem_trywait 相当于 P 操作,它们都能将信号量的值减一,两者的区别在于若信号量小于零时,sem_wait 将会阻塞进程,而 sem_trywait 则会立即返回。

    sem_post 相当于 V 操作,它将信号量的值加一同时发出信号唤醒等待的进程。

    sem_getvalue 用于得到信号量的值。

    sem_destroy 用于删除信号量。


(3)函数格式

表 9.11 列出了 sem_init 函数的语法要点。

表 9.12 列出了 sem_wait 等函数的语法要点。

(4)使用实例

下面实例 1 使用信号量实现了上一实例中对 lock_var 的操作,在这里使用的是互斥操作,也就是只使用一个信号量来实现。代码如下所示:

 

/*sem_mutex.c*/

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <pthread.h>

#include <errno.h>

#include <sys/ipc.h>

#include <semaphore.h>


int lock_var;

time_t end_time;

sem_t sem;


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)+30;


    /*初始化信号量为

    ret=sem_init(&sem,0,1);

    if(ret!=0)

    {

        perror("sem_init");

    }


    /*创建两个线程*/

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

    if(ret!=0)

        perror("pthread cread1");


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

    if(ret!=0)

        perror("pthread cread2");


    pthread_join(id1,NULL);

    pthread_join(id2,NULL);


    exit(0);

}



void pthread1(void *arg)

{

    int i;


    while(time(NULL) < end_time)

    {

        /*信号量减一,P 操作*/

        sem_wait(&sem);

        for(i=0;i<2;i++)

        {

            sleep(1);

            lock_var++;

            printf("lock_var=%d\n",lock_var);

        }

        printf("pthread1:lock_var=%d\n",lock_var);

        /*信号量加一,V 操作*/

        sem_post(&sem);

        sleep(1);

    }

}


void pthread2(void *arg)

{

    int nolock=0;

    int ret;


    while(time(NULL) < end_time)

    {

        /*信号量减一,P 操作*/

        sem_wait(&sem);

        printf("pthread2:pthread1 got lock;lock_var=%d\n",lock_var);

        /*信号量加一,V 操作*/

        sem_post(&sem);

        sleep(3);

    }

}

 

程序运行结果如下所示:

[root@(none) tmp]# ./sem_num

lock_var=1

lock_var=2

pthread1:lock_var=2

pthread2:pthread1 got lock;lock_var=2

lock_var=3

lock_var=4

pthread1:lock_var=4

pthread2:pthread1 got lock;lock_var=4

 

接下来是通过两个信号量来实现两个线程间的同步,仍然完成了以上实例中对lock_var的操作。代码如下所示:


/*sem_syn.c*/

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <pthread.h>

#include <errno.h>

#include <sys/ipc.h>

#include <semaphore.h>


int lock_var;

time_t end_time;

sem_t sem1,sem2;


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)+30;


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

    ret=sem_init(&sem1,0,1);

    ret=sem_init(&sem2,0,0);

    if(ret!=0)

    {

        perror("sem_init");

    }


    /*创建两个线程*/

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

    if(ret!=0)

        perror("pthread cread1");


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

    if(ret!=0)

        perror("pthread cread2");


    pthread_join(id1,NULL);

    pthread_join(id2,NULL);

    exit(0);

}


void pthread1(void *arg)

{

    int i;


    while(time(NULL) < end_time)

    {

        /*P 操作信号量

        sem_wait(&sem2);

        for(i=0;i<2;i++)

        {

            sleep(1);

            lock_var++;

            printf("lock_var=%d\n",lock_var);

        }

        printf("pthread1:lock_var=%d\n",lock_var);

        /*V 操作信号量

        sem_post(&sem1);

        sleep(1);

    }

}


void pthread2(void *arg)

{

    int nolock=0;

    int ret;


    while(time(NULL) < end_time)

    {

        /*P 操作信号量

        sem_wait(&sem1);

        printf("pthread2:pthread1 got lock;lock_var=%d\n",lock_var);


        /*V 操作信号量

        sem_post(&sem2);

        sleep(3);

    }

}

 


   从以下结果中可以看出,该程序确实实现了先运行线程二,再运行线程一

 

[root@(none) tmp]# ./sem_num

pthread2:pthread1 got lock;lock_var=0

lock_var=1

lock_var=2

pthread1:lock_var=2

pthread2:pthread1 got lock;lock_var=2

lock_var=3

lock_var=4

pthread1:lock_var=4


9.3  实验内容——“生产者消费者”实验

1.实验目的

“生产者消费者”问题是一个著名的同时性编程问题的集合。通过编写经典的“生产者消费者”问题的实验,读者可以进一步熟悉 Linux 中多线程编程,并且掌握用信号量处理线程间的同步互斥问题。

 

2.实验内容

“生产者消费者”问题描述如下。

有一个有限缓冲区和两个线程:生产者和消费者。他们分别把产品放入缓冲区和从缓冲区中拿走产品。当一个生产者在缓冲区满时必须等待,当一个消费者在缓冲区空时页必须等待。它们之间的关系如下图所示:


这里要求用有名管道来模拟有限缓冲区,用信号量来解决生产者消费者问题中的同步和互斥问题。

思考与练习

通过查找资料,查看主流的嵌入式操作系统(如嵌入Linux,Vxworks  等)是如何处理多线程操作的?

第10章 嵌入式 Linux 网络编程

10.1  TCP/IP 协议概述

10.1.1  OSI 参考模型及 TCP/IP 参考模型

读者一定都听说过著名的 OSI 协议参考模型,它是基于国际标准化组织(ISO)的建议发展起来的,从上到下共分为 7 层:应用层、表示层、会话层、传输层、网络层、数据链路层及物理层。这个 7 层的协议模型虽然规定得非常细致和完善,但在实际中却得不到广泛的应用,其重要的原因之一就在于它过于复杂。但它仍是此后很多协议模型的基础,这种分层架构的思想在很多领域都得到了广泛的应用。

与此相区别的 TCP/IP 协议模型从一开始就遵循简单明确的设计思路,它将 TCP/IP 的 7层协议模型简化为 4 层,从而更有利于实现和使用。TCP/IP 的协议参考模型和 OSI 协议参考模型的对应关系如下图 10.1 所示。

下面分别对者 TCP/IP 的 4 层模型进行简要介绍。

 

 网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络信息传输单元。

 网络层:负责将数据帧封装成 IP 数据报,并运行必要的路由算法。

 传输层:负责端对端之间的通信会话连接与建立。传输协议的选择根据数据传输方式而定。

 应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。

10.1.2  TCP/IP 协议族

虽然 TCP/IP 名称只包含了两个协议,但实际上,TCP/IP 是一个庞大的协议族,它包括了各个层次上的众多协议,图 10.2 列举了各层中一些重要的协议,并给出了各个协议在不同层次中所处的位置如下。

    ARP:用于获得同一物理网络中的硬件主机地址。

    MPLS:多协议标签协议,是很有发展前景的下一代网络协议。

    IP:负责在主机和网络之间寻址和路由数据包。

    ICMP:用于发送报告有关数据包的传送错误的协议。

    IGMP:被 IP 主机用来向本地多路广播路由器报告主机组成员的协议。

    TCP:为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。

    UDP:提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量数据,可靠性则由应用层来负责。

10.1.3  TCP 和

在此主要介绍在网络编程中涉及到的传输层 TCP 和 UDP 协议。

1.TCP

(1)概述

同其他任何协议栈一样,TCP 向相邻的高层提供服务。因为 TCP 的上一层就是应用层,因此,TCP 数据传输实现了从一个应用程序到另一个应用程序的数据传递。应用程序通过编程调用 TCP 并使用 TCP 服务,提供需要准备发送的数据,用来区分接收数据应用的目的地址和端口号。

通常应用程序通过打开一个 socket 来使用 TCP 服务,TCP 管理到其他 socket 的数据传递。可以说,通过 IP 的源/目的可以惟一地区分网络中两个设备的关联,通过 socket 的源/目的可以惟一地区分网络中两个应用程序的关联。

(2)三次握手协议

TCP 对话通过三次握手来初始化的。三次握手的目的是使数据段的发送和接收同步,告诉其他主机其一次可接收的数据量,并建立虚连接。

下面描述了这三次握手的简单过程。

 初始化主机通过一个同步标志置位的数据段发出会话请求。

 接收主机通过发回具有以下项目的数据段表示回复:同步标志置位、即将发送的数据段的起始字节的顺序号、应答并带有将收到的下一个数据段的字节顺序号。

 请求主机再回送一个数据段,并带有确认顺序号和确认号。

图 10.3 就是这个流程的简单示意图。

 

TCP 实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据报时,它将启动计时器。当该数据报到达目的地后,接收方的 TCP 实体向回发送一个数据报,其中包含有一个确认序号,它意思是希望收到的下一个数据报的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据报。

(3)TCP 数据报头

图 10.4 给出了 TCP 数据报头的格式。

TCP 数据报头的含义如下所示。

 源端口、目的端口:16 位长。标识出远端和本地的端口号。

 序号:32 位长。标识发送的数据报的顺序。

 确认号:32 位长。希望收到的下一个数据报的序列号。

  TCP 头长:4 位长。表明 TCP 头中包含多少个 32 位字。

  6 位未用。

  ACK:ACK 位置 1 表明确认号是合法的。如果 ACK 为 0,那么数据报不包含确认信息,确认字段被省略。

  PSH:表示是带有 PUSH 标志的数据。接收方因此请求数据报一到便可送往应用程序而不必等到缓冲区装满时才传送。

  RST:用于复位由于主机崩溃或其他原因而出现的错误的连接。还可以用于拒绝非法的数据报或拒绝连接请求。

  SYN:用于建立连接。

  FIN:用于释放连接。

 窗口大小:16 位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。

 校验和:16 位长。是为了确保高可靠性而设置的。它校验头部、数据和伪 TCP 头部之和。

 可选项:0 个或多个 32 位字。包括最大 TCP 载荷,窗口比例、选择重发数据报等选项。

 

2.UDP

(1)概述

UDP    即用户数据报协议,它是一种无连接协议,因此不需要像 TCP 那样通过三次握手来建立一个连接。同时,一个 UDP 应用可同时作为应用的客户或服务器方。由于 UDP 协议并不需要建立一个明确的连接,因此建立 UDP 应用要比建立 TCP 应用简单得多。

UDP 协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是在网络质量越来越高的今天,UDP   的应用得到了大大的增强。它比 TCP 协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用 UDP 协议。

(2)UDP 数据包头

UDP 数据包头如下图 10.5 所示。

图 10.5

 源地址、目的地址:16 位长。标识出远端和本地的端口号。

 数据报的长度是指包括报头和数据部分在内的总的字节数。因为报头的长度是固定的,所以该域主要用来计算可变长度的数据部分(又称为数据负载)。

 

3.协议的选择

协议的选择应该考虑到以下 3 个方面。

(1)对数据可靠性的要求

对数据要求高可靠性的应用需选择 TCP 协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用可选择 UDP 传送。

(2)应用的实时性

由于   TCP 协议在传送过程中要进行三次握手、重传确认等手段来保证数据传输的可靠性。使用 TCP 协议会有较大的时延,因此不适合对实时性要求较高的应用,如 VOIP、视频监控等。相反,UDP 协议则在这些应用中能发挥很好的作用。

(3)网络的可靠性

由于 TCP 协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网络状况不是很好的情况下需选用 TCP 协议(如在广域网等情况),但是若在网络状况很好的情况下(如局域网等)就不需要再采用 TCP 协议,选择 UDP 协议来减少网络负荷

10.2  网络基础编程

10.2.1  socket 概述

1.socket 定义

在 Linux 中的网络编程是通过 socket 接口来进行的。人们常说的 socket 接口是一种特殊的 I/O,它也是一种文件描述符。每一个 socket 都用一个半相关描述{协议,本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议,本地地址、本地端口、远程地址、远程端口}。socket 也有一个类似于打开文件的函数调用,该函数返回一个整型的 socket 描述符,随后的连接建立、数据传输等操作都是通过 socket 来实现的。

 

2.socket 类型

常见的 socket 有 3 种类型如下。

(1)流式 socket(SOCK_STREAM)

流式套接字提供可靠的、面向连接的通信流;它使用 TCP 协议,从而保证了数据传输的正确性和顺序性。

(2)数据报 socket(SOCK_DGRAM)

数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议 UDP。

(3)原始

原始套接字允许对底层协议如 IP 或 ICMP 进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。

10.2.2  地址及顺序处理

1.地址结构相关处理

(1)数据结构介绍

下面首先介绍两个重要的数据类型:sockaddr 和 sockaddr_in,这两个结构类型都是用来保存 socket 信息的,如下所示:

   

struct sockaddr

{

unsigned short sa_family; /*地址族*/

char sa_data[14]; /*14 字节的协议地址,包含该 socket 的 IP 地址和端口号。*/

};


struct sockaddr_in

{

short int sa_family; /*地址族*/

unsigned short int sin_port; /*端口号*/

struct in_addr sin_addr; /*IP 地址*/

unsigned char sin_zero[8]; /*填充 0 以保持与 struct sockaddr 同样大小*/

};


struct in_addr类型定义在netinet/in.h中,如下

struct in_addr{

in_addr_t s_addr;  /*in_addr_t是一个32位整数*/

};

 这两个数据类型是等效的,可以相互转化,通常 sockaddr_in 数据类型使用更为方便。在建立 socketadd 或 sockaddr_in 后,就可以对该 socket 进行适当的操作了。

 (2)结构字段

表 10.1 列出了该结构 sa_family 字段可选的常见值。

 

对了解 sockaddr_in 其他字段的含义非常清楚,具体的设置涉及到其他函数,在后面会有详细讲解。

 

 2.数据存储优先顺序

(1)函数说明

计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet 上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对这两个字节存储优先顺序进行相互转化。这里用到了四个函数:htons、ntohs、htonl、ntohl。这四个地址分别实现网络字节序和主机字节序的转化,这里的 h 代表 host,n 代表 network,s 代表 short,l 代表 long。通常 16 位的 IP 端口号用 s 代表,而 IP 地址用 l 来代表。

 (2)函数格式说明

表 10.2 列出了这 4 个函数的语法格式。

 

 

注意,调用该函数只是使其得到相应的字节序,用户不需清楚该系统的主机字节序和网络字节序是否真正相等。如果是相同不需要转换的话,该系统的这些函数会定义成空宏。


3.地址格式转化

(1)函数说明

通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制IPv6 地址),而在通常使用的 socket 编程中所使用的则是二进制值,这就需要将这两个数值进行转换。这里在 IPv4 中用到的函数有 inet_aton、inet_addr 和 inet_ntoa,而 IPv4 和 IPv6 兼容的函数有 inet_pton 和 inet_ntop。由于 IPv6 是下一代互联网的标准协议,因此,本书讲解的函数都能够同时兼容 IPv4 和 IPv6,但在具体举例时仍以 IPv4 为例。

这里 inet_pton 函数是将点分十进制地址映射为二进制地址,而 inet_ntop 是将二进制地址映射为点分十进制地址。

(2)函数格式

表 10.3 列出了 inet_pton 函数的语法要点。

表 10.4 列出了 inet_ntop 函数的语法要点。

 

 

4.名字地址转化

(1)函数说明

通常,人们在使用过程中都不愿意记忆冗长的 IP 地址,尤其到 IPv6 时,地址长度多达128 位,那时就更加不可能一次次记忆那么长的 IP 地址了。因此,使用主机名将会是很好的选 择 。在    Linux  中 , 同样 有 一些 函数 可以 实现主 机 名和 地址 的转 化 ,最 为常 见的 有gethostbyname、gethostbyaddr、getaddrinfo 等,它们都可以实现 IPv4 和 IPv6 的地址和主机名之间的转化。其中 gethostbyname 是将主机名转化为 IP 地址,gethostbyaddr 则是逆操作,是将 IP 地址转化为主机名,另外 getaddrinfo 还能实现自动识别 IPv4 地址和 IPv6 地址。

gethostbyname 和 gethostbyaddr 都涉及到一个 hostent 的结构体,如下所示:

 

Struct hostent{

      char *h_name;/*正式主机名*/

      char **h_aliases;/*主机别名*/

      int h_addrtype;/*地址类型*/

      int h_length;/*地址长度*/

      char **h_addr_list;/*指向IPv4 IPv6 的地址指针数组*/

}

 

调用该函数后就能返回 hostent 结构体的相关信息。

getaddrinfo 函数涉及到一个 addrinfo 的结构体,如下所示:

 

struct addrinfo{

      int ai_flags;/*AI_PASSIVE,AI_CANONNAME;*/

      int ai_family;/*地址族*/

      int ai_socktype;/*socket 类型*/

      int ai_protocol;/*协议类型*/

      size_t ai_addrlen;/*地址长度*/

      char *ai_canoname;/*主机名*/

      struct sockaddr *ai_addr;/*socket 结构体*/

      struct addrinfo *ai_next;/*下一个指针链表*/

}

 

hostent 结构体而言,addrinfo 结构体包含更多的信息。

(2)函数格式

表 10.5 列出了 gethostbyname 函数的语法要点。

表    gethostbyname 函数语法要点

 

调用该函数时可以首先对 addrinfo 结构体中的 h_addrtype 和 h_length 进行设置,若为 IPv4 可设置为 AF_INET 和 4;若为 IPv6 可设置为 AF_INET6 和 16;若不设置则默认为 IPv4 地址类型。

表 10.6 列出了 getaddrinfo 函数的语法要点。

在调用之前,首先要对 hints 服务线索进行设置。它是一个 addrinfo 结构体,表 10.7 列举了该结构体常见的选项值。

 

 

注意

1  通常服务器端在调用 getaddrinfo 之前,ai_flags 设置 AI_PASSIVE,用于 bind 函数(用于端口和地址的绑定后面会讲到),主机名 nodename 通常会设置为 NULL。

2  客户端调用 getaddrinfo 时,ai_flags 一般不设置 AI_PASSIVE,但是主机名 nodename 和服务名 servname(端口)则应该不为空。

3  即使不设置 ai_flags 为 AI_PASSIVE,取出的地址也并非不可以被 bind,很多程序中 ai_flags直接设置为 0,即 3 个标志位都不设置,这种情况下只要 hostname 和 servname 设置的没有问题就可以正确 bind。

 

(3)使用实例

下面的实例给出了 getaddrinfo 函数用法的示例,在后面小节中会给出 gethostbyname 函数用法的例子。

/*getaddrinfo.c*/

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <netdb.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <sys/socket.h>


int main()

{

struct addrinfo hints,*res=NULL;

int rc;


memset(&hints,0,sizeof(hints));


/*设置 addrinfo 结构体中各参数*/

hints.ai_family=PF_UNSPEC;

hints.ai_socktype=SOCK_DGRAM;

hints.ai_protocol=IPPROTO_UDP;


/*调用 getaddinfo 函数*/

rc=getaddrinfo("127.0.0.1","123",&hints,&res);

if (rc != 0)

{

    perror("getaddrinfo");

    exit(1);

}

else

    printf("getaddrinfo success\n");

}

 

运行结果如下所示:

[root@(none) tmp]# getaddrinfo success

10.2.3  socket 基础编程

(1)函数说明

进行 socket 编程的基本函数有 socket、bind、listen、accept、send、sendto、recv、recvfrom这几个,其中对于客户端和服务器端以及 TCP 和 UDP 的操作流程都有所区别,这里先对每个函数进行一定的说明,再给出不同情况下使用的流程图。

socket:该函数用于建立一个 socket 连接,可指定 socket 类型等信息。在建立了 socket连接之后,可对 socketadd 或 sockaddr_in 进行初始化,以保存所建立的 socket 信息。

bind:该函数是用于将本地  IP 地址绑定端口号的,若绑定其他地址则不能成功。另外,它主要用于 TCP 的连接,而在 UDP 的连接中则无必要。

connect:该函数在 TCP 中是用于 bind 之后的 client 端,用于与服务器端建立连接,而在 UDP 中由于没有了 bind 函数,因此用 connect 有点类似 bind 函数的作用。

send 和 recv:这两个函数用于接收和发送数据,可以用在 TCP 中,也可以用在 UDP中。当用在 UDP 时,可以在 connect 函数建立连接之后再用。

sendto 和 recvfrom:这两个函数的作用与 send 和 recv 函数类似,也可以用在 TCP 和UDP 中。当用在 TCP 时,后面的几个与地址有关参数不起作用,函数作用等同于 send 和 recv;当用在 UDP 时,可以用在之前没有使用 connect 的情况时,这两个函数可以自动寻找制定地址并进行连接。

服务器端和客户端使用 TCP 协议的流程图如图 10.6 所示。

 

服务器端和客户端使用 UDP 协议的流程图如图 10.7 所示。

 

(2)函数格式

表 10.8 列出了 socket 函数的语法要点。

表 10.9 列出了 bind 函数的语法要点。






表                      bind 函数语法要点

表 10.10 列出了 listen 函数的语法要点。

 

表 10.11 列出了 accept 函数的语法要点。

注:该函数返回一个整型新套接字与客户通信,而老套接字继续监听更多的请求,直到关闭。

表 10.12 列出了 connect 函数的语法要点。

 

表 10.13 列出了 send 函数的语法要点。

 

表 10.14 列出了 recv 函数的语法要点。

 

表 10.15 列出了 sendto 函数的语法要点。

 

表 10.16 列出了 recvfrom 函数的语法要点。

 

(3)使用实例

该实例分为客户端和服务器端,其中服务器端首先建立起  socket,然后调用本地端口的绑定,接着就开始与客户端建立联系,并接收客户端发送的消息。客户端则在建立 socket 之后调用 connect 函数来建立连接。源代码如下所示:

/*server.c*/

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <unistd.h>

#include <netinet/in.h>


#define SERVPORT 3333

#define BACKLOG 10

#define MAX_CONNECTED_NO 10

#define MAXDATASIZE 5


int main()

{

struct sockaddr_in server_sockaddr,client_sockaddr;

int sin_size,recvbytes;

int sockfd,client_fd;

char buf[MAXDATASIZE];


/*建立 socket 连接*/

if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)

{

    perror("socket");

    exit(1);

}  

printf("socket success!,sockfd=%d\n",sockfd);


/*设置  结构体中相关参数*/

server_sockaddr.sin_family=AF_INET;

server_sockaddr.sin_port=htons(SERVPORT);

server_sockaddr.sin_addr.s_addr=INADDR_ANY;

bzero(&(server_sockaddr.sin_zero),8);


/*绑定函数

if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1)

{

    perror("bind");

    exit(1);

}

printf("bind success!\n");


/*调用 listen 函数*/

if(listen(sockfd,BACKLOG)==-1)

{

    perror("listen");

    exit(1);

}

printf("listening....\n");


/*调用 accept 函数等待客户端的连接*/

if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1)

{

    perror("accept");

    exit(1);

}


/*调用 recv 函数接收客户端的请求*/

if((recvbytes=recv(client_fd,buf,MAXDATASIZE,0))==-1)

{

    perror("recv");

    exit(1);

}


printf("received a connection :%s\n",buf);

close(sockfd);

}



/*client.c*/

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <netdb.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <sys/socket.h>


#define SERVPORT 3333

#define MAXDATASIZE 100


main(int argc,char *argv[])

{

int sockfd,sendbytes;

char buf[MAXDATASIZE];

struct hostent *host;

struct sockaddr_in serv_addr;


if(argc < 2)

{

    fprintf(stderr,"Please enter the server's hostname!\n");

    exit(1);

    }


/*地址解析函数*/

if((host=gethostbyname(argv[1]))==NULL)

{

    perror("gethostbyname");

    exit(1);

}


/*创建

if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)

{

    perror("socket");

    exit(1);

}


/*设置  结构体中相关参数*/

serv_addr.sin_family=AF_INET;

serv_addr.sin_port=htons(SERVPORT);

serv_addr.sin_addr=*((struct in_addr *)host->h_addr);

bzero(&(serv_addr.sin_zero),8);


/*调用 connect 函数主动发起对服务器端的连接*/

if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1)

{

    perror("connect");

    exit(1);

}


/*发送消息给服务器端*/

if((sendbytes=send(sockfd,"hello",5,0))==-1)

{

    perror("send");

    exit(1);

}

close(sockfd);

}


在运行时需要先启动服务器端,再启动客户端。这里可以把服务器端下载到开发板上,客户端在宿主机上运行,然后配置双方的 IP 地址,确保在双方可以通信(如使用 ping 命令验证)的情况下运行该程序即可。


[root@(none) tmp]# ./server

socket success!,sockfd=3

bind success!

listening....

received a connection :hello

[root@130 test]# ./client 59.64.128.1

10.3  网络高级编程

在实际情况中,人们往往遇到多个客户端连接服务器端的情况。由于之前介绍的如connet、recv、send 都是阻塞性函数,若资源没有准备好,则调用该函数的进程将进入睡眠状态,这样就无法处理 I/O 多路复用的情况了。本节给出了两种解决 I/O 多路复用的解决方法,这两个函数都是之前学过的 fcntl 和 select(请读者先复习第 6 章中的相关内容)。可以看到,由于在 Linux 中把 socket 也作为一种特殊文件描述符,这给用户的处理带来了很大的方便。

1.fcntl

函数 fcntl 针对 socket 编程提供了如下的编程特性。

非阻塞 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_NONBLOCK。

信号驱动 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_ASYNC。

下面是用 fcntl 设置为非阻塞 I/O 的使用实例:

 

/*fcntl.c*/

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/wait.h>

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <sys/un.h>

#include <sys/time.h>

#include <sys/ioctl.h>

#include <unistd.h>

#include <netinet/in.h>

#include <fcntl.h>


#define SERVPORT 3333

#define BACKLOG 10

#define MAX_CONNECTED_NO 10

#define MAXDATASIZE 100


int main()

{

struct sockaddr_in server_sockaddr,client_sockaddr;

int sin_size,recvbytes,flags;

int sockfd,client_fd;

char buf[MAXDATASIZE];


if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)

{

    perror("socket");

    exit(1);

}  

printf("socket success!,sockfd=%d\n",sockfd);


server_sockaddr.sin_family=AF_INET;

server_sockaddr.sin_port=htons(SERVPORT);

server_sockaddr.sin_addr.s_addr=INADDR_ANY;

bzero(&(server_sockaddr.sin_zero),8);


if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1)

{

    perror("bind");

    exit(1);

}  

printf("bind success!\n");


if(listen(sockfd,BACKLOG)==-1)

{

    perror("listen");

    exit(1);

}

printf("listening....\n");


/*调用 fcntl 函数设置非阻塞参数*/

if((flags=fcntl( sockfd, F_GETFL))<0)

    perror("fcntl F_SETFL");


flags |= O_NONBLOCK;

if(fcntl(sockfd,F_SETFL,flags)<0)

    perror("fcntl");


while(1)

{

    sin_size=sizeof(struct sockaddr_in);

    if((client_fd=accept(sockfd,(struct sockaddr*)&client_sockaddr,&sin_size))==-1)

    {

        perror("accept");

        exit(1);

    }


    if((recvbytes=recv(client_fd,buf,MAXDATASIZE,0))==-1)

    {

        perror("recv");

        exit(1);

    }

    buf[recvbytes] = 0/* ‘\0’ */;

    printf("received a connection :%s",buf);


    close(client_fd);

    exit(1);

}/*while*/

}

运行该程序,结果如下所示:

[root@(none) tmp]]# ./fcntl

socket success!,sockfd=3

bind success!

listening....

accept: Resource temporarily unavailable

可以看到,当 accept 的资源不可用时,程序就会自动返回。

 

2.select

使用 fcntl 函数虽然可以实现非阻塞 I/O 或信号驱动 I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的 CPU 资源。在这里可以使用 select函数来解决这个问题,同时,使用 select 函数还可以设置等待的时间,可以说功能更加强大。下面是使用 select 函数的服务器端源代码:

 

/*select_socket.c*/

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/wait.h>

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <sys/un.h>

#include <sys/time.h>

#include <sys/ioctl.h>

#include <unistd.h>

#include <netinet/in.h>


#define SERVPORT 3333

#define BACKLOG 10

#define MAX_CONNECTED_NO 10

#define MAXDATASIZE 100


int main()

{

struct sockaddr_in server_sockaddr,client_sockaddr;

int sin_size,recvbytes;

fd_set readfd;

fd_set writefd;

int sockfd,client_fd;

char buf[MAXDATASIZE];


if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)

{

    perror("socket");

    exit(1);

}

printf("socket success!,sockfd=%d\n",sockfd);


server_sockaddr.sin_family=AF_INET;

server_sockaddr.sin_port=htons(SERVPORT);

server_sockaddr.sin_addr.s_addr=INADDR_ANY;

bzero(&(server_sockaddr.sin_zero),8);

if(bind(sockfd,(struct   sockaddr    *)&server_sockaddr,sizeof(struct sockaddr))==-1)

{

    perror("bind");

    exit(1);

}

printf("bind success!\n");


if(listen(sockfd,BACKLOG)==-1)

{

    perror("listen");

    exit(1);

}

printf("listening....\n");


/*将调用 socket 函数的描述符作为文件描述符*/

FD_ZERO(&readfd);

FD_SET(sockfd,&readfd);


while(1)

{

    sin_size=sizeof(struct sockaddr_in);

    /*调用 select 函数*/

    if(select(MAX_CONNECTED_NO,&readfd,NULL,NULL,(struct timeval *)0)>0)

    {

        if(FD_ISSET(sockfd,&readfd)>0)

        {

            if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1)

            {

                perror("accept");

                exit(1);

            }


            if((recvbytes=recv(client_fd,buf,MAXDATASIZE,0))==-1)

            {

                perror("recv");

                exit(1);

            }


            if(read(client_fd,buf,MAXDATASIZE)<0)

            {

                perror("read");

                exit(1);

            }

            printf("received a connection :%s",buf);

        }/*if*/

        close(client_fd);

    }/*select*/

}/*while*/

}


运行该程序时,可以先启动服务器端,再反复运行客户端程序即可,服务器端运行结果如下所示:

[root@(none) tmp]# ./server2   

socket success!,sockfd=3

bind success!

listening....

received a connection :hello

received a connection :hello


10.4  实验内容——NTP 协议实现

1.实验目的

通过实现 NTP 协议的练习,进一步掌握 Linux 下网络编程,并且提高协议的分析与实现

能力,为参与完成综合性项目打下良好的基础。

 

2.实验内容

Network Time Protocol(NTP)协议是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS 等)做同步化,它可以提供高精确度的时间校正(LAN 上与标准间差小于 1 毫秒,WAN 上几十毫秒),且可用加密确认的方式来防止恶毒的协议入侵。NTP 提供准确时间,首先要有准确的时间来源,这一时间应该是国际标准时间    UTC。NTP 获得 UTC 的时间来源可以是原子钟、天文台、卫星,也可以从 Internet 上获取。这样就有了准确而可靠的时间源。时间是按 NTP 服务器的等级传播。按照距离外部   源的远近将所有服务器归入不同的 Stratun(层)中。Stratum-1 在顶层,有外部 UTC 接入,而 Stratum-2则从 Stratum-1 获取时间,Stratum-3 从 Stratum-2 获取时间,以此类推,但 Stratum 层的总数限制在 15 以内。所有这些服务器在逻辑上形成阶梯式的架构相互连接,而 Stratum-1 的时间服务器是整个系统的基础。

进行网络协议实现时最重要的是了解协议数据格式。NTP 数据包有 48 个字节,其中

包头 16 字节,时间戳 32 个字节。其协议格式如图 10.9 所示。

其协议字段的含义如下所示。

    LI:跳跃指示器,警告在当月最后一天的最终时刻插入的迫近闺秒(闺秒)。

    VN:版本号。

    Mode:模式。该字段包括以下值:0-预留;1-对称行为;3-客户机;4-服务器;

5-广播;6-NTP 控制信息。

    Stratum:对本地时钟级别的整体识别。

    Poll:有符号整数表示连续信息间的最大间隔。

    Precision:有符号整数表示本地时钟精确度。

    Root  Delay:有符号固定点序号表示主要参考源的总延迟,很短时间内的位 15 到 16间的分段点。

    Root Dispersion:无符号固定点序号表示相对于主要参考源的正常差错,很短时间内的位 15 到 16 间的分段点。

    Reference Identifier:识别特殊参考源。

    Originate Timestamp:这是向服务器请求分离客户机的时间,采用 64 位时标格式。

    Receive Timestamp:这是向服务器请求到达客户机的时间,采用 64 位时标格式。

    Transmit Timestamp:这是向客户机答复分离服务器的时间,采用 64 位时标格式。

    Authenticator(Optional):当实现了 NTP 认证模式时,主要标识符和信息数字域就包括已定义的信息认证代码(MAC)信息。

由于 NTP 协议中涉及到比较多的时间相关的操作,为了简化实现过程,本实验仅要求实现 NTP 协议客户端部分的网络通信模块,也就是构造 NTP 协议字段进行发送和接收,最后与时间相关的操作不需进行处理。

思考与练习

实现一个小型模拟的路由器,就是接收从某个 IP 地址的连接,再把该请求转发到另一个IP 地址的主机上去。

第11章 嵌入式Linux设备驱动编程

11.1 设备驱动编程基础

例1、OURS开发箱上8段数码管驱动程序源文件xa270_serial_led_drv.c如下:

include <linux/config.h>

#include <linux/kernel.h>

#include <linux/sched.h>

#include <linux/timer.h>

#include <linux/init.h>

#include <linux/devfs_fs_kernel.h>

#include <linux/module.h>

#include <asm/hardware.h>

#include <linux/interrupt.h>    /* for in_interrupt */

#include <linux/timer.h>

#include <linux/init.h>

#include <linux/delay.h>        /* for udelay */

#include <linux/modversions.h>

#include <linux/version.h>

#include <asm/io.h>

#include <asm/irq.h>

#include <asm/hardware.h>

#include <asm/uaccess.h>

 

// HELLO DEVICE MAJOR

#define SERIAL_LED_MAJOR  105

#define OURS_HELLO_DEBUG

#define VERSION         "PXA270RP-serial_led-V1.00-090214"

void showversion(void)

{

        printk("*********************************************\n");

        printk("\t %s \t\n", VERSION);

        printk("*********************************************\n\n");

}

void write_bit(int data)

{

       GPCR2 |= (0x1 << 27);    

       if((data & 0x80) == 0x80)

       {

              GPSR2 |= (0x1 << 26);            

       }

       else

       {

              GPCR2 |= (0x1 << 26);

       }

       GPSR2 |= (0x1 << 27);

}

void write_byte(int data)

{

        int i;

        for(i=0;i<8;i++)

        {

                write_bit( data << i );

        }

}

// ------------------- READ ------------------------

ssize_t SERIAL_LED_read (struct file * file ,char * buf, size_t count, loff_t * f_ops)

{

       #ifdef OURS_HELLO_DEBUG

              printk ("SERIAL_LED_read [ --kernel--]\n");

       #endif             

       return count;

}    

// ------------------- WRITE -----------------------

ssize_t SERIAL_LED_write (struct file * file ,const char * buf, size_t count, loff_t * f_ops)

{

       #ifdef OURS_HELLO_DEBUG

               printk ("SERIAL_LED_write [ --kernel--]\n");

        #endif

//     printk("data=%x\n",* buf);

       write_byte(* buf);

       return count;

}    

// ------------------- IOCTL -----------------------

ssize_t SERIAL_LED_ioctl (struct inode * inode ,struct file * file, unsigned int cmd,unsigned long data)

{

       #ifdef OURS_HELLO_DEBUG

               printk ("SERIAL_LED_ioctl [ --kernel--]\n");

        #endif    

       return 0;

}

// ------------------- OPEN ------------------------

ssize_t SERIAL_LED_open (struct inode * inode ,struct file * file)

{

       #ifdef OURS_HELLO_DEBUG

               printk ("SERIAL_LED_open [ --kernel--]\n");

        #endif    

       MOD_INC_USE_COUNT;  

       return 0;

}    

// ------------------- RELEASE/CLOSE ---------------

ssize_t SERIAL_LED_release (struct inode  * inode ,struct file * file)

{

       #ifdef OURS_HELLO_DEBUG

               printk ("SERIAL_LED_release [ --kernel--]\n");

        #endif

       MOD_DEC_USE_COUNT;

       return 0;

}

// -------------------------------------------------

struct file_operations SERIAL_LED_ops ={    

       open:             SERIAL_LED_open,   

       read:       SERIAL_LED_read,    

       write:             SERIAL_LED_write,   

       ioctl:              SERIAL_LED_ioctl,    

       release:    SERIAL_LED_release, 

};

void gpio_init(void)

{

        GPDR2 = GPDR2 | (0x3<<26);

//     printk("GPDR2 = %x\n",GPDR2);

}

// ------------------- INIT ------------------------

static int __init HW_SERIAL_LED_init(void)

{

    int ret = -ENODEV;   

       ret = devfs_register_chrdev(SERIAL_LED_MAJOR, "serial_led", &SERIAL_LED_ops);

       showversion();

       if( ret < 0 )

       {

              printk (" pxa270 init_module failed with %d\n [ --kernel--]", ret);     

              return ret;

       }

       else

       {

              printk(" pxa270 serial_led_driver register success!!! [ --kernel--]\n");

       }

       gpio_init();    

       return ret;

}

static int __init pxa270_SERIAL_LED_init(void)

{

    int  ret = -ENODEV;

       #ifdef OURS_HELLO_DEBUG

                printk ("pxa270_HELLO_CTL_init [ --kernel--]\n");

        #endif

    ret = HW_SERIAL_LED_init();

    if (ret)

      return ret;

    return 0;

}

static void __exit cleanup_SERIAL_LED(void)

{

       #ifdef OURS_HELLO_DEBUG

               printk ("cleanup_HELLO_ctl [ --kernel--]\n");

        #endif    

       devfs_unregister_chrdev (SERIAL_LED_MAJOR, "serial_led" );

}

MODULE_DESCRIPTION("serial_led driver module");

MODULE_AUTHOR("liduo");

MODULE_LICENSE("GPL");

module_init(pxa270_SERIAL_LED_init);

module_exit(cleanup_SERIAL_LED);

 

测试程序simple_test_driver.c如下:

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include <fcntl.h>      // open() close()

#include <unistd.h>     // read() write()

 

#define DEVICE_NAME "/dev/serial_led"

//------------------------------------- main ----------------------------------------------------------------

int main(void)

{

    int fd;

       int ret;

       int i,count;

    int  buf[10] = { 0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};                        //0    1    2    3    4    5    6    7   8    9

       int  data[10];

    printf("\nstart serial_led driver test\n\n");

    fd = open(DEVICE_NAME, O_RDWR); 

       printf("fd = %d\n",fd);

        if (fd == -1)

        {

                printf("open device %s error\n",DEVICE_NAME);

        }

        else

        {

               while(1)

               {

                       for(count=0;count<10;count++)

                     {

                            data[0] = buf[count];

                               ret=write(fd,data,1);

                               sleep(1);

                       }

               }

        }

        ret = close(fd);

        printf ("ret=%d\n",ret);

        printf ("close serial_led driver test\n");

        return 0;

}// end main

 

例2、数码点阵驱动程序pxa270_led_ary_drv.c如下:

#include <linux/config.h>

#include <linux/kernel.h>

#include <linux/sched.h>

#include <linux/timer.h>

#include <linux/init.h>

#include <linux/devfs_fs_kernel.h>

#include <linux/module.h>

 

#include <asm/hardware.h>

#include <asm/io.h>

 

// LED DEVICE MAJOR

#define SIMPLE_LED_MAJOR  99

//#define OURS_LED_DEBUG

#define VERSION         "PXA270RP-ledary-V1.00-090214"

 

static long *ioremap_addr;

void showversion(void)

{

        printk("*********************************************\n");

        printk("\t %s \t\n", VERSION);

        printk("*********************************************\n\n");

 

}

// ------------------- READ ------------------------

ssize_t SIMPLE_LED_read (struct file * file ,char * buf, size_t count, loff_t * f_ops)

{

       #ifdef OURS_LED_DEBUG

              printk ("SIMPLE_LED_read [ --kernel--]\n");

       #endif             

 

       return count;

}    

 

// ------------------- WRITE -----------------------

ssize_t SIMPLE_LED_write (struct file * file ,const char * buf, size_t count, loff_t * f_ops)

{

      

       int tmp_buf;

       #ifdef OURS_LED_DEBUG

               printk ("SIMPLE_LED_write [ --kernel--]\n");

        #endif

      

       // -------------------------------------------        

       tmp_buf = buf[1];

       tmp_buf = tmp_buf<<8;

       tmp_buf = tmp_buf | buf[0];

       #ifdef OURS_LED_DEBUG

                printk("tmp = %x\n",tmp_buf);  

       #endif

 

       outw(tmp_buf,ioremap_addr);

       // -------------------------------------------

 

       return count;

}    

 

// ------------------- IOCTL -----------------------

ssize_t SIMPLE_LED_ioctl (struct inode * inode ,struct file * file, unsigned int cmd,unsigned long data)

{

       #ifdef OURS_LED_DEBUG

               printk ("SIMPLE_LED_ioctl [ --kernel--]\n");

        #endif

      

       return 0;

      

}

 

// ------------------- OPEN ------------------------

ssize_t SIMPLE_LED_open (struct inode * inode ,struct file * file)

{

       #ifdef OURS_LED_DEBUG

               printk ("SIMPLE_LED_open [ --kernel--]\n");

        #endif

      

       MOD_INC_USE_COUNT;

      

       return 0;

}    

 

// ------------------- RELEASE/CLOSE ---------------

ssize_t SIMPLE_LED_release (struct inode  * inode ,struct file * file)

{

       #ifdef OURS_LED_DEBUG

               printk ("SIMPLE_LED_release [ --kernel--]\n");

        #endif

 

       outw(0x0000,ioremap_addr); // close the led ary, all led off

 

       MOD_DEC_USE_COUNT;

 

       return 0;

}

 

// -------------------------------------------------

struct file_operations LED_ctl_ops ={

 

      

       open:             SIMPLE_LED_open,

      

       read:       SIMPLE_LED_read,

      

       write:             SIMPLE_LED_write,

      

       ioctl:              SIMPLE_LED_ioctl,

      

       release:    SIMPLE_LED_release,

      

};

 

// ------------------- INIT ------------------------

static int __init HW_LED_CTL_init(void)

{

           int ret = -ENODEV;

//       char inbyte;  

 

       static long *addr_status;

       ret = devfs_register_chrdev(SIMPLE_LED_MAJOR, "led_ary_ctl", &LED_ctl_ops);

 

       showversion();

    

       if( ret < 0 )

       {

              printk (" pxa270: init_module failed with %d\n [ --kernel--]", ret);    

              return ret;

       }

       else

       {

              printk(" pxa270 led_driver register success!!! [ --kernel--]\n");

       }

 

       // ---------------------------------------------

     addr_status=ioremap(0x08000018,0x0f);

          outb(0x20,addr_status);  //select cpld2      

       ioremap_addr=ioremap(0x0c00c000,0x0f); //

        outw(0x00ff,ioremap_addr); // open led ary, all led on

       #ifdef OURS_LED_DEBUG

               printk("remap address = %x  [ --kernel--]\n",ioremap_addr);

       #endif

 

       // ---------------------------------------------

 

       return ret;

}

 

static int __init pxa270_LED_CTL_init(void)

{

    int  ret = -ENODEV;

 

       #ifdef OURS_LED_DEBUG

                printk ("pxa270_LED_CTL_init [ --kernel--]\n");

        #endif

 

    ret = HW_LED_CTL_init();

    if (ret)

      return ret;

    return 0;

}

 

static void __exit cleanup_LED_ctl(void)

{

       static long *addr_status;

       #ifdef OURS_LED_DEBUG

               printk ("cleanup_LED_ctl [ --kernel--]\n");

        #endif

       outw(0x0000,ioremap_addr);

     

     addr_status=ioremap(0x08000018,0x0f);

 

          outb(0x00,addr_status);  //select cpld2      

       devfs_unregister_chrdev (SIMPLE_LED_MAJOR, "led_ary_ctl" );

 

}

MODULE_DESCRIPTION("simple led driver module");

MODULE_AUTHOR("liduo");

MODULE_LICENSE("GPL");

 

module_init(pxa270_LED_CTL_init);

module_exit(cleanup_LED_ctl);

测试文件simple_test_driver.c内容如下:

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

 

#include <fcntl.h>      // open() close()

#include <unistd.h>     // read() write()

 

 

#define DEVICE_NAME "/dev/led_ary_ctl"

 

 

//------------------------------------- main ----------------------------------------------------------------

int main(void)

{

       int fd;

       int ret;

       unsigned char buf[2] ;

       unsigned char c,r;

       int i,j;

      

       // begin of led ary

       c = 1;

       r = 1;

 

        printf("\nstart led_driver test\n\n");

 

        fd = open(DEVICE_NAME, O_RDWR);

      

       printf("fd = %d\n",fd);

      

        if (fd == -1) {

              printf("open device %s error\n",DEVICE_NAME);

        }

        else {

              for (i=1;i<=8;i++) {

                     buf[0]=c;

                     buf[1]=~r; // row

                     for (j=1;j<=8;j++) {

                            write(fd,buf,2);

                            printf ("buf[0],buf[1]: [%x,%x]\n",buf[0],buf[1]);

                            usleep(200000); // sleep 0.2 second

                            c = c<<1;

                            buf[0]=c; // column

                         }

                     c = 1;

                    r = r<<1;

             }

              // close

              ret = close(fd);

              printf ("ret=%d\n",ret);

              printf ("close led_driver test\n");

       }

 

        return 0;

}// end main

11.1.1 Linux设备驱动概述

概念:设备驱动程序是操作系统最基本的组成部分之一,操作系统通过各种驱动程序来控制硬件设备的,它屏蔽了硬件功能的实现细节,为用户提供统一接口。

       Linux中将所有的设备都当作文件进行处理,可以像操作普通文件一样对其操作,主要有三类设备:字符设备如并口设备、虚拟控制台、帧缓存,块设备如IDE硬盘、SCSI硬盘、光驱等,网络设备如网卡。

11.1.2 Linux内核模块编程

Linux内核中采用可加载的模块化设计,也就是将最基本的核心代码编译在内核中,其他的代码可以编译到内核中,或者编译为内核的模块文件,在需要时动态加载。设备驱动程序属于内核的一部分,可能以两种方式被编译和加载:一种是直接编译进内核,随同Linux启动时加载,如CPU、PCI总线、TCP/IP、APM等驱动程序;另一种是编译成可加载和删除的模块,使用命令insmod加载,命令rmmod卸载,前提条件是驱动程序模块必须提供相应的模块加载函数(一般以__init标志声明)和卸载函数(一般以__exit标志声明),以及模块许可证声明(MODULE_LICENSE("GPL"))。

11.2 字符设备驱动编程

11.2.1 字符设备驱动编写流程

1、申请主设备号MAJOR和次设备号MINOR;

2、定义struct file_operations的各个函数;

3、定义struct file_operations数据结构;

4、定义__init函数和__exit函数,在其中分别实现设备的登记注册和注销;

5、模块许可证声明;

6、module_init(pxa270_SERIAL_LED_init)

module_exit(cleanup_SERIAL_LED)

11.2.2 重要数据结构

file_operations,file,inode,其详细内容参看有关文档。

11.2.3 设备驱动程序主要组成

参看“驱动程序编写”节。

11.3 GPIO驱动程序实例

参看OURS实验箱提供的GPIO驱动程序实例。

11.4 按键驱动程序实例

参看OURS实验箱提供的按键驱动程序实例。

第12章 Qt图形编程

12.1 嵌入式GUI简介

GUI( 图形用户界面)是指计算机与其使用者之间的对话接口,它的存在为使用者提供了友好便利的界面,并大大方便了非专业用户的使用,使得人们从烦琐的命令中解脱出来,可以通过窗口、菜单方便地进行操作。

在嵌入式系统中,GUI的地位越来越重要,嵌入式GUI应具备以下特点:体积小;运行时耗用系统资源小;上层接口与硬件无关,高度可移植;高可靠性;在某些场合应具备实用性。

12.1.1 Qt/Embedded

       Qt/Embedded是Trolltech(奇趣)公司为嵌入式系统开发的Qt版本。从Qt 4.1版本开始,Qt/Embedded改名为Qtopia Core,又从Qt 4.4.1版本开始,又改名为Qt for Embedded Linux。其优点是:

1、以开发包的形式提供,包括了图形设计器、Makefile制作工具、字体国际化工具、Qt的C++类库等。

2、支持跨平台(如Microsoft Windows、MacOS X、Linux、Solaris等)。

3、类库支持跨平台(封装了适应不同操作系统的访问细节)。

4、模块化,可任意裁减(最小可裁减到几百KB,但这时已基本失去了使用价值)。

缺点是:结构过于复杂臃肿,很难进行底层的扩充、定制和移植。

12.1.2 MiniGUI

       MiniGUI是面向实时嵌入式系统的轻量级图形用户界面支持系统。目前,MiniGU已广泛应用于手持信息终端、机顶盒、工业控制系统及工业仪表、便携式多媒体播放器、查询终端等产品和领域,支持跨操作系统、跨硬件平台如ARM、PowerPC、MIPS等。

12.2 Qt/Embedded开发入门

 

12.2.1 Qt/Embedded介绍

1、架构如下图所示。

 

应用程序源代码

Qt API

Qt/Embedded

Qt/X11

Qt/XLib

X Windows Server

帧缓冲

Linux内核

Qt/Embedded与Qt/X11的Linux版本的比较

2、Qt的开发环境

Qt桌面版本:Qt/X11或Qt/Windows,结合大家熟知的Visual C++或Borland C++就可以进行开发了。`

3、Qt的支撑工具

       Qmake和Qt designer(图形设计器)

12.2.2 Qt/Embedded信号和槽机制

1、机制概述

       信号(signals)和槽机制是Qt的核心机制,是Qt自行定义的一种通信机制,独立于标准的C/C++语言。所有从QObject或其子类(如QWidget)派生的类都能够包含信号和槽。当对象改变状态时,信号就由该对象发射(Emit)出去了,它不知道另一端是是谁在接收这个信号。槽函数用于接收信号,它们是普通的对象成员函数,一个槽函数并不知道是否有任何信号与自己相连接。

对象1

信号1

信号2

 

对象2

信号1

槽1

槽2

对象3

 

槽1

槽2



对象间信号与插槽的关系


       用户可以将多个信号与单个槽函数连接,也可以将单个信号与多个槽函数连接,甚至将一个信号与另外一个信号相连接。其间的关系如下图所示。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2、信号与插槽实现

信号的定义:

signals:

void mySignal();

void mySignal(int x);

void mySignalParam(int x,int y);

槽函数的定义:

public slots:

void mySlot();

void mySlot(int x);

void mySlotParam(int x,int y);

信号与插槽关联(通过QObject对象的connect()函数实现)

QLabel *label=new QLabel;

QScrollBar *scroll=new QScrollBar;

QObject::connect(scroll,SIGNAL(valueChanged(int)),label,SLOT(setNum(int)));

12.2.3 搭建Qt/Embedded开发环境

Qt/X11、Qt/Windows、Qt/Embedded安装和环境变量的设置请参考相关文档。

12.2.4 Qt/Embedded窗口部件

Qt提供了一整套的窗口部件,它们组合起来可用于创建用户界面的可视元素。窗口部件是QWidget或其子类的实例,用户自定义的窗口通过子类化得到,如下图所示。

 

 

 

 

 

 

 

 

 

 

 

 

 


例:Hello Qt

#include <QApplication>

#include <QPushButton>

 

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

{

       QApplication app(argc,argv);

       QPushButton *quitBt=new QPushButton(“Hello Qt!”);

       quitBt->show();

       QObject::connect(quitBt,SIGNAL(clicked()),&app,SLOT(quit()));

       return app.exec();

}

12.2.5 Qt/Embedded图形界面编程

对于初学者来说,使用designer工具设计程序界面非常方便快捷,如秒表程序stopwatch的界面文件digclock.ui效果如下图所示。


播放器程序mediaplayer界面文件videoface.ui效果如下图所示。

 

 

对应的.h文件代码自动生成,但比较复杂,为了熟悉代码,完全可以用代码的形式生成用户界面,如通讯录程序addressbook中的部分代码如下:

addressBook::addressBook(QWidget *parent)

{

       setWindowTitle(tr("this is addressbook."));

       setFixedSize(lcdWidth,lcdHeight);

      

       contacts=new QMap<QString,QString>;

      

       saveBt=new QPushButton(tr("save"))  ;

       cancelBt=new QPushButton(tr("cancel"));

       findBt=new QPushButton(tr("find"));

      

       contactName=new QLabel(tr("name"));

       contactTele=new QLabel(tr("telephone"));

      

       nameDetail=new QLineEdit;

       teleNo=new QTextEdit ;

 

       QHBoxLayout *hlayout=new QHBoxLayout;

       hlayout->addWidget(saveBt,Qt::AlignCenter);

       hlayout->addWidget(cancelBt,Qt::AlignJustify);

       hlayout->addWidget(findBt,Qt::AlignCenter);

      

 

       QGridLayout *layout=new QGridLayout;

       layout->addWidget(contactName,0,0,Qt::AlignRight);

       layout->addWidget(nameDetail,0,1);

       layout->addWidget(contactTele,1,0,Qt::AlignTop);

       layout->addWidget(teleNo,1,1,Qt::AlignLeft);

       layout->addLayout(hlayout,2,1);

 

       setLayout(layout);

       connect(saveBt,SIGNAL(clicked()),this,SLOT(saveInform()));

       connect(cancelBt,SIGNAL(clicked()),this,SLOT(cancelInform()));

       connect(findBt,SIGNAL(clicked()),this,SLOT(findName()));

}

 

12.2.6 Qt/Embedded对话框设计

以菜单程序menu为例,包括菜单QMenu、工具栏ToolBar、窗口控件如文本框QTextEdit、对话框如打开文件、保存文件等,详细内容参看该程序源码。

12.3 实验内容:使用Qt编写“Hello World”程序