星期三, 十月 31, 2007

S3C2410下的Linux键盘驱动分析

内核初始化的时候会调用chr_dev_init 之后会有这样的调用过程

0 && image.height>0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" style="width: 184px; height: 239px;" src="http://docs.google.com/File?id=dcmkbdfm_40ctgbvcc2">
真正的键盘部分的初始化从kbd_init开始 kbd_init 初始化键盘状态结构体(struct kbd_struct), 这个结构体主要记录shift状态, led状态等. 将各个tty对应的struct kbd_struct全部初始化. 即每个tty都有自己的独立的struct kbd_struct 得到tty的结构体指针 键盘的硬件初始化(kbd_init) 允许键盘处理的后半部分(tasklet_enable)
调度键盘处理的后半部分(keyboard_tasklet),
keyboard_tasklet主要是处理键值(kbd_processkeycode)和处理led指示灯(kbd_leds)的动作.
kbd_processkeycode主要判断各种状态, 将扫描值映射成键值, 将其放入tty的键值队列,
以及autorepeat动作(如果键盘是处于raw mode就不会处理autorepeat, 因为控制台是希望得到键盘的原始状态).
autorepeat主要是通过一个timer来实现的,这个timer调用key_callback,这个函数又再次调用
keyboard_tasklet, 这样只要按着键就会不断有新键值进入tty的键值队列.
对于键盘处理的后半部分(keyboard_tasklet), 我们只要记住一点: 这部分操作时间可能会比较长, 所以需要与中断响应分开处理.
向电源管理模块注册(pm_register)
OK, 我们再来看看键盘的硬件初始化
kbd_init被定义成与(s3c2410_kbd_init)相同,s3c2410_kbd_init会调用HW_kbd_init,
设置键盘接口界面(这里使用的是SPI). 包括IO口控制,SPI触发模式,频率等.
写一些测试数据(SPI_WRITE)
将key_interrupt注册到EINT1.这里好像有点奇怪,怎么会注册到外部中断呢?分析可能是这个版本对应的外挂SPI键盘在有按键按下时,会产生一个中断,通知S3C2410去读SPI接口
key_interrupt
选通外挂键盘
等待外挂键盘准本就绪
写一个空数据到SPI总线上

待所有数据输出完成(在输出完成的同时,输入数据(即键盘的扫描值)也进入到S3C2410.
这一步其实是效率很低的,因为是在FIQ响应中,所以所有中断都被禁止,
而SPI总线的速率为99.12K,那么我们等待所有数据输出完成就需要(8/99.12)毫秒,也就是80.7uS,
而在这段时间里CPU是什么也干不了的.不过幸亏,没有几个人会用到SPI总线的外挂键盘吧.
对扫描值做一些判断与处理, 最后调度后半处理函数keyboard_tasklet

Read More...

SPI协议

SPI协议简介

SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片 的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协 议,比如AT91RM9200.

SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,它们是SDI(数据输入),SDO(数据输出),SCK(时钟),CS(片选)。

(1)SDO – 主设备数据输出,从设备数据输入

(2)SDI – 主设备数据输入,从设备数据输出

(3)SCLK – 时钟信号,由主设备产生

(4)CS – 从设备使能信号,由主设备控制

其中CS是控制芯片是否被选中的,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),对此芯片的操作才有效。这就允许在同一总线上连接多个SPI设备成为可能。

接下来就负责通讯的3根线了。通讯是通过数据交换完成的,这里先要知道SPI是串行通讯协 议,也就是说数据是一位一位的传输的。这就是SCK时钟线存在的原因,由SCK提供时钟脉冲,SDI,SDO则基于此脉冲完成数据传输。数据输出通过 SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取。完成一位数据传输,输入也使用同样原理。这样,在至少8次时钟信号的改变 (上沿和下沿为一次),就可以完成8位数据的传输。

要注意的是,SCK信号线只由主设备控制,从设备不能控制信号线。同样,在一个基于SPI的 设备中,至少有一个主控设备。这样传输的特点:这样的传输方式有一个优点,与普通的串行通讯不同,普通的串行通讯一次连续传送至少8位数据,而SPI允许 数据一位一位的传送,甚至允许暂停,因为SCK时钟线由主控设备控制,当没有时钟跳变时,从设备不采集或传送数据。也就是说,主设备通过对SCK时钟线的 控制可以完成对通讯的控制。SPI还是一个数据交换协议:因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。不同的SPI设备的实现 方式不尽相同,主要是数据改变和采集的时间不同,在时钟信号上沿或下沿采集有不同定义,具体请参考相关器件的文档。

在点对点的通信中,SPI接口不需要进行寻址操作,且为全双工通信,显得简单高效。在多个从设备的系统中,每个从设备需要独立的使能信号,硬件上比I2C系统要稍微复杂一些。

最后,SPI接口的一个缺点:没有指定的流控制,没有应答机制确认是否接收到数据。

AT91RM9200的SPI接口主要由4个引脚构成:SPICLK、MOSI、MISO及 /SS,其中SPICLK是整个SPI总线的公用时钟,MOSI、MISO作为主机,从机的输入输出的标志,MOSI是主机的输出,从机的输入,MISO 是主机的输入,从机的输出。/SS是从机的标志管脚,在互相通信的两个SPI总线的器件,/SS管脚的电平低的是从机,相反/SS管脚的电平高的是主机。 在一个SPI通信系统中,必须有主机。SPI总线可以配置成单主单从,单主多从,互为主从。

SPI的片选可以扩充选择16个外设,这时PCS输出=NPCS,说NPCS0~3接4-16译码器,这个译码器是需要外接4-16译码器,译码器的输入为NPCS0~3,输出用于16个外设的选择。

Read More...

星期五, 十月 26, 2007

转:编译的一点体会

从源码包安装软件最重要的就是仔细阅读README INSTALL等说明文件

它会告诉你怎样才能成功安装
通常从源码包安装软件的步骤是:
tar jxvf gtk+-2.4.13.tar.bz2 解开源码包
cd gtk+-2.4.13/ 进入源码目录
./configure 似乎在某些环境下./configure会造成终端退出
而使用. configure则会正常运行,如果有这个现象,就试试 . configure

通过configure程序猜测主机信息,最终建立Makefile,以完成make,所以如果./configure不成功
而去make的话,就会出现"make: *** No targets specified and no makefile found. Stop."
make 当./configure成功结束后,就开始正式编译程序了.
make install 编译成功后使用make install安装
make uninstall 某些软件支持卸载,可能使用该方法卸载,如果支持的话,通常会在README中写到(似乎比较少)

configure程序带有很多参数,可以通过 ./configure --help 查看详细内容,通常位于前面的是常规configure的
参数说明,末尾是该程序的可用参数说明。
./configure --prefix=/usr 指定安装目录,通常从源码包编译安装的软件默认会放在/usr/local下
因为这是FHS(Filesystem Hierarchy Standard)的规定,不知道什么是FHS?看看这篇文章吧:
http://www.pathname.com/fhs/pub/fhs-2.3.html 相信它会让你对linux系统结构有更好的理解,很值得读读。

再说一下几个关系到能否成功编译的东东:/etc/ld.so.conf ldconfig PKG_CONFIG_PATH

首先说下/etc/ld.so.conf:

这个文件记录了编译时使用的动态链接库的路径。
默认情况下,编译器只会使用/lib和/usr/lib这两个目录下的库文件
如果你安装了某些库,比如在安装gtk+-2.4.13时它会需要glib-2.0 >= 2.4.0,辛苦的安装好glib后
没有指定 --prefix=/usr 这样glib库就装到了/usr/local下,而又没有在/etc/ld.so.conf中添加/usr/local/lib
这个搜索路径,所以编译gtk+-2.4.13就会出错了
对于这种情况有两种方法解决:
一:在编译glib-2.4.x时,指定安装到/usr下,这样库文件就会放在/usr/lib中,gtk就不会找不到需要的库文件了
对于安装库文件来说,这是个好办法,这样也不用设置PKG_CONFIG_PATH了 (稍后说明)

二:将/usr/local/lib加入到/etc/ld.so.conf中,这样安装gtk时就会去搜索/usr/local/lib,同样可以找到需要的库
将/usr/local/lib加入到/etc/ld.so.conf也是必须的,这样以后安装东东到local下,就不会出现这样的问题了。
将自己可能存放库文件的路径都加入到/etc/ld.so.conf中是明智的选择 ^_^
添加方法也极其简单,将库文件的绝对路径直接写进去就OK了,一行一个。例如:
/usr/X11R6/lib
/usr/local/lib
/opt/lib

再来看看ldconfig是个什么东东吧 :

它是一个程序,通常它位于/sbin下,是root用户使用的东东。具体作用及用法可以man ldconfig查到
简单的说,它的作用就是将/etc/ld.so.conf列出的路径下的库文件 缓存到/etc/ld.so.cache 以供使用
因此当安装完一些库文件,(例如刚安装好glib),或者修改ld.so.conf增加新的库路径后,需要运行一下/sbin/ldconfig
使所有的库文件都被缓存到ld.so.cache中,如果没做,即使库文件明明就在/usr/lib下的,也是不会被使用的,结果
编译过程中抱错,缺少xxx库,去查看发现明明就在那放着,搞的想大骂computer蠢猪一个。 ^_^
我曾经编译KDE时就犯过这个错误,(它需要每编译好一个东东,都要运行一遍),所以

切记改动库文件后一定要运行一下ldconfig,在任何目录下运行都可以。


再来说说 PKG_CONFIG_PATH这个变量吧:

经常在论坛上看到有人问"为什么我已经安装了glib-2.4.x,但是编译gtk+-2.4.x 还是提示glib版本太低阿?
为什么我安装了glib-2.4.x,还是提示找不到阿?。。。。。。"都是这个变量搞的鬼。
先来看一个编译过程中出现的错误 (编译gtk+-2.4.13):

checking for pkg-config... /usr/bin/pkg-config
checking for glib-2.0 >= 2.4.0 atk >= 1.0.1 pango >= 1.4.0... Package glib-2.0 was not found in the pkg-config search path.
Perhaps you should add the directory containing `glib-2.0.pc'
to the PKG_CONFIG_PATH environment variable
No package 'glib-2.0' found

configure: error: Library requirements (glib-2.0 >= 2.4.0 atk >= 1.0.1 pango >= 1.4.0) not met; consider adjusting the PKG_CONFIG_PATH environment variable if your libraries are in a nonstandard prefix so pkg-config can find them.
[root@NEWLFS gtk+-2.4.13]#
很明显,上面这段说明,没有找到glib-2.4.x,并且提示应该将glib-2.0.pc加入到PKG_CONFIG_PATH下。
究竟这个pkg-config PKG_CONFIG_PATH glib-2.0.pc 是做什么的呢? let me tell you ^_^
先说说它是哪冒出来的,当安装了pkgconfig-x.x.x这个包后,就多出了pkg-config,它就是需要PKG_CONFIG_PATH的东东
pkgconfig-x.x.x又是做什么的? 来看一段说明:

代码:
The pkgconfig package contains tools for passing the include path and/or library paths to build tools during the make file execution.

pkg-config is a function that returns meta information for the specified library.

The default setting for PKG_CONFIG_PATH is /usr/lib/pkgconfig because of the prefix we use to install pkgconfig. You may add to PKG_CONFIG_PATH by exporting additional paths on your system where pkgconfig files are installed. Note that PKG_CONFIG_PATH is only needed when compiling packages, not during run-time.
我想看过这段说明后,你已经大概了解了它是做什么的吧。
其实pkg-config就是向configure程序提供系统信息的程序,比如软件的版本啦,库的版本啦,库的路径啦,等等
这些信息只是在编译其间使用。你可以 ls /usr/lib/pkgconfig 下,会看到许多的*.pc,用文本编辑器打开
会发现类似下面的信息:

prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include

glib_genmarshal=glib-genmarshal
gobject_query=gobject-query
glib_mkenums=glib-mkenums

Name: GLib
Description: C Utility Library
Version: 2.4.7
Libs: -L${libdir} -lglib-2.0
Cflags: -I${includedir}/glib-2.0 -I${libdir}/glib-2.0/include

明白了吧,configure就是靠这些信息判断你的软件版本是否符合要求。并且得到这些东东所在的位置,要不去哪里找呀。
不用我说你也知道为什么会出现上面那些问题了吧。

解决的办法很简单,设定正确的PKG_CONFIG_PATH,假如将glib-2.x.x装到了/usr/local/下,那么glib-2.0.pc就会在
/usr/local/lib/pkgconfig下,将这个路径添加到PKG_CONFIG_PATH下就可以啦。并且确保configure找到的是正确的
glib-2.0.pc,就是将其他的lib/pkgconfig目录glib-2.0.pc干掉就是啦。(如果有的话 ^-^)
设定好后可以加入到~/.bashrc中,例如:
PKG_CONFIG_PATH=/opt/kde-3.3.0/lib/pkgconfig:/usr/lib/pkgconfig:/usr/local/pkgconfig:
/usr/X11R6/lib/pkgconfig
[root@NEWLFS ~]#echo $PKG_CONFIG_PATH
/opt/kde-3.3.0/lib/pkgconfig:/usr/lib/pkgconfig:/usr/local/pkgconfig:/usr/X11R6/lib/pkgconfig

从上面可以看出,安装库文件时,指定安装到/usr,是很有好处的,无论是/etc/ld.so.conf还是PKG_CONFIG_PATH
默认都会去搜索/usr/lib的,可以省下许多麻烦,不过从源码包管理上来说,都装在/usr下
管理是个问题,不如装在/usr/local下方便管理
其实只要设置好ld.so.conf,PKG_CONFIG_PATH路径后,就OK啦 ^_^

另外某些软件因为版本原因(比如emacs-21.3),在gcc-3.4.x下编译无法成功,(make 出错)
使用低版本的gcc就可能编译通过。
可能是因为gcc-3.3.x和gcc-3.4.x变化很大的缘故吧。

暂时想到了这么多,先记下这些吧,如果你对源码包编译有了一点的了解,就不枉我打了这么半天字啦。 ^_^

另外./configure 通过,make 出错,遇到这样的问题比较难办,只能凭经验查找原因,比如某个头文件没有找到,
这时候要顺着出错的位置一行的一行往上找错,比如显示xxxx.h no such file or directory 说明缺少头文件
然后去google搜。
或者找到感觉有价值的错误信息,拿到google去搜,往往会找到解决的办法。还是开始的那句话,要仔细看README,INSTALL
程序如何安装,需要什么依赖文件,等等。

另外对于newbie来说,编译时,往往不知道是否成功编译通过,而编译没有通过就去make install
必然会出错,增加了解决问题的复杂性,可以通过下面方法检查是否编译成功:

一:编译完成后,输入echo $? 如果返回结果为0,则表示正常结束,否则就出错了
echo $? 表示 检查上一条命令的退出状态,程序正常退出 返回0,错误退出返回非0。
二:编译时,可以用&&连接命令, && 表示"当前一条命令正常结束,后面的命令才会执行",就是"与"啦。
这个办法很好,即节省时间,又可防止出错。例:
./configure --prefix=/usr && make && make install

编译DOSBOX时出现"cdrom.h:20:23: SDL_sound.h: No such file or directory"

今天忽然想回味下经典DOS游戏,于是编译这个DOSBOX模拟器,README中说明需要SDL_SOUND
于是下载,安装,很顺利,没有指定安装路径,于是默认的安装到了/usr/local/
当编译DOSBOX make 时,出现如下错误:
if g++ -DHAVE_CONFIG_H -I. -I. -I../.. -I../../include -I/usr/include/SDL -D_REENTRANT -march=pentium4 -O3 -pipe -fomit-frame-pointer -MT dos_programs.o -MD -MP -MF ".deps/dos_programs.Tpo" -c -o dos_programs.o dos_programs.cpp; \
then mv -f ".deps/dos_programs.Tpo" ".deps/dos_programs.Po"; else rm -f ".deps/dos_programs.Tpo"; exit 1; fi
In file included from dos_programs.cpp:30:
cdrom.h:20:23: SDL_sound.h: No such file or directory <------错误的原因在这里
In file included from dos_programs.cpp:30:
cdrom.h:137: error: ISO C++ forbids declaration of `Sound_Sample' with no type
cdrom.h:137: error: expected `;' before '*' token
make[3]: *** [dos_programs.o] Error 1
make[3]: Leaving directory `/root/software/dosbox-0.63/src/dos'
make[2]: *** [all-recursive] Error 1
make[2]: Leaving directory `/root/software/dosbox-0.63/src'
make[1]: *** [all-recursive] Error 1
make[1]: Leaving directory `/root/software/dosbox-0.63'
make: *** [all] Error 2
[root@NEWLFS dosbox-0.63]#
看来是因为cdrom.h没有找到SDL_sound.h这个头文件
所以出现了下面的错误,但是我明明已经安装好了SDL_sound阿?
经过查找,在/usr/local/include/SDL/下找到了SDL_sound.h
看来dosbox没有去搜寻/usr/local/include/SDL下的头文件,既然找到了原因,就容易解决啦

[root@NEWLFS dosbox-0.63]#ln -s /usr/local/include/SDL/SDL_sound.h /usr/include

做个链接到/usr/include下,这样DOSBOX就可以找到了,顺利编译成功,回味仙剑ing....^_^
曾经编译Xorg-6.8.1的时候,也出现找不到freetype.h的问题,原因也是如此。
编译安装软件时,经常遇到类似的情况,都是因为找不到需要的头文件而出现错误,也许是因为
没有安装相关的头文件,或者是安装了但没有找到,如上例。
找不到的情况:做个链接到/usr/include下,就可以了。
没安装的情况:去google找什么东东包括该头文件,安装上就应该可以了。
通常错误提示也都是"No such file or directory",所以编译失败时要好好找找错误信息哦。
错误信息总是在Error上面不远的,耐心点 ^_^

不修改/etc/ld.so.conf使用非默认路径下的库文件-----LD_LIBRARY_PATH

环境变量LD_LIBRARY_PATH列出了查找共享库时除了默认路径之外的其他路径。
如果不想修改或无法修改(无root权限)/etc/ld.so.conf而使用其他路径下的库文件
就需要设置LD_LIBRARY_PATH了,例:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/lib
这样就可以使用/opt/lib下的库文件啦。当然还是修改/etc/ld.so.conf方便。

先写到这吧,再有编译感想再增加 ^_^

Read More...

星期四, 十月 25, 2007

Build package flow

Read More...

星期一, 十月 22, 2007

转:使用vmware+linux如何共享WIndows下资源

昨天从www.arm9.net上下载了nano2410_sdk40的光盘更新,.tgz后缀的压缩文件,我把文件刻录到光盘后.
再在linux下挂载光驱,结果刻录后的文件无法tar.好在vmware有共享文件夹,这样我们就可以随意调用windows下的资源。
 
我宿主系统是xp+VMware+RedHat 9
虚拟机不认识宿主系统的盘,因为从逻辑上讲那是另一台机器了。所以想用mount挂载其他的盘是不行的。

方法如下:
打开VMare-->"虚拟机"-->"设置"-->"选项"-->"共享文件夹"启用,并添加需要用到的主机资源文件所在位置.然后安装vmware   tools,则/mnt/hgfs下就是共享的目录。
以下是安装VMtools的方法
1、以ROOT身份进入LINUX
  2、按下 CTRL+ALT组合键,进入主操作系统,点击VMWARE状态栏安装提示,或者点击 SETTING菜单下的ENABLE VMWARE TOOLS子菜单。
  3、确认安装VMWARE TOOLS。
  这时我们并没有真正的安装上了VMWARE TOOLS软件包,如果您点击菜单:DEVICES,您就会发现光驱的菜单文字变为:ide1:0-> C:\\Program Files\\VMware\\VMware Workstation\\Programs\\linux.iso,这表示VMWARE将LINUX的ISO映象文件作为了虚拟机的光盘
  4、鼠标点击LINUX界面,进入LINUX。
  5、运行如下命令,注意大小写。
  mount -t iso9660 /dev/cdrom /mnt
  加载CDROM设备,这时如果进入 /mnt 目录下,你将会发现多了一个文件:vmwaretools-5.5.1-19175.tar.gz。这就是WMWARE TOOLS的LINUX软件包,也就是我们刚才使用WINISO打开LINUX.ISO文件所看到的。
  cp /mnt/vmwaretools-5.5.1-19175.tar.gz/tmp
  将该软件包拷贝到LINUX的 TMP目录下。
  umount /dev/cdrom
  卸载CDROM。
  cd /tmp
  进入TMP目录
  tar xvzf vmwaretools-5.5.1-19175.tar.gz
  解压缩该软件包,默认解压到vmware-tools-distrib目录下(与文件名同名)。
  cd vmware-linux-tools
  进入解压后的目录
  ./VMTools-install.pl
  运行安装命令。

Read More...

星期五, 十月 19, 2007

转:使用 sed 编辑器

使用 sed 编辑器
作者:Emmett Dulaney

sed 编辑器是 Linux 系统管理员的工具包中最有用的资产之一,
因此,有必要彻底地了解其应用

Linux 操作系统最大的一个好处是它带有各种各样的实用工具。存在如此之多不同的实用工具,几乎不可能知道并了解所有这些工具。可以简化关键情况下操作的一个实用工具是 sed。它是任何管理员的工具包中最强大的工具之一,并且可以证明它自己在关键情况下非常有价值。

sed 实用工具是一个“编辑器”,但它与其它大多数编辑器不同。除了不面向屏幕之外,它还是非交互式的。这意味着您必须将要对数据执行的命令插入到命令行或要处 理的脚本中。当显示它时,请忘记您在使用 Microsoft Word 或其它大多数编辑器时拥有的交互式编辑文件功能。sed 在一个文件(或文件集)中非交互式、并且不加询问地接收一系列的命令并执行它们。因而,它流经文本就如同水流经溪流一样,因而 sed 恰当地代表了流编辑器。 它可以用来将所有出现的 "Mr. Smyth" 修改为 "Mr. Smith",或将 "tiger cub" 修改为 "wolf cub"。流编辑器非常适合于执行重复的编辑,这种重复编辑如果由人工完成将花费大量的时间。其参数可能和一次性使用一个简单的操作所需的参数一样有限, 或者和一个具有成千上万行要进行编辑修改的脚本文件一样复杂。sed 是 Linux 和 UNIX 工具箱中最有用的工具之一,且使用的参数非常少。

sed 的工作方式

sed 实用工具按顺序逐行将文件读入到内存中。然后,它执行为该行指定的所有操作,并在完成请求的修改之后将该行放回到内存中,以将其转储至终端。完成了这一行 上的所有操作之后,它读取文件的下一行,然后重复该过程直到它完成该文件。如同前面所提到的,默认输出是将每一行的内容输出到屏幕上。在这里,开始涉及到 两个重要的因素—首先,输出可以被重定向到另一文件中,以保存变化;第二,源文件(默认地)保持不被修改。sed 默认读取整个文件并对其中的每一行进行修改。不过,可以按需要将操作限制在指定的行上。

该实用工具的语法为:

 

sed [options] '{command}' [filename]

 

在这篇文章中,我们将浏览最常用的命令和选项,并演示它们如何工作,以及它们适于在何处使用。

替换命令

sed 实用工具以及其它任何类似的编辑器的最常用的命令之一是用一个值替换另一个值。用来实现这一目的的操作的命令部分语法是:

 

's/{old value}/{new value}/'

 

因而,下面演示了如何非常简单地将 "tiger" 修改为 "wolf":

 

$ echo The tiger cubs will meet on Tuesday after school | sed 's/tiger/wolf/'The wolf cubs will meet on Tuesday after school$

 

注意如果输入是源自之前的命令输出,则不需要指定文件名—同样的原则也适用于 awk、sort 和其它大多数 Linux\UNIX 命令行实用工具程序。

多次修改

如果需要对同一文件或行作多次修改,可以有三种方法来实现它。第一种是使用 "-e" 选项,它通知程序使用了多条编辑命令。例如:

 

$ echo The tiger cubs will meet on Tuesday after school | sed -e 's/tiger/wolf/' -e 's/after/before/'The wolf cubs will meet on Tuesday before school$

 

这是实现它的非常复杂的方法,因此 "-e" 选项不常被大范围使用。更好的方法是用分号来分隔命令:

 

$ echo The tiger cubs will meet on Tuesday after school | sed 's/tiger/wolf/; s/after/before/'The wolf cubs will meet on Tuesday before school $

 

注 意分号必须是紧跟斜线之后的下一个字符。如果两者之间有一个空格,操作将不能成功完成,并返回一条错误消息。这两种方法都很好,但许多管理员更喜欢另一种 方法。要注意的一个关键问题是,两个撇号 (' ') 之间的全部内容都被解释为 sed 命令。直到您输入了第二个撇号,读入这些命令的 shell 程序才会认为您完成了输入。这意味着可以在多行上输入命令—同时 Linux 将提示符从 PS1 变为一个延续提示符(通常为 ">")—直到输入了第二个撇号。一旦输入了第二个撇号,并且按下了 Enter 键,则处理就进行并产生相同的结果,如下所示:

 

$ echo The tiger cubs will meet on Tuesday after school | sed '> s/tiger/wolf/> s/after/before/'The wolf cubs will meet on Tuesday before school$

 

全局修改

让我们开始一次看似简单的编辑。假定在要修改的消息中出现了多次要修改的项目。默认方式下,结果可能和预期的有所不同,如下所示:

 

$ echo The tiger cubs will meet this Tuesday at the same timeas the meeting last Tuesday | sed 's/Tuesday/Thursday/'The tiger cubs will meet this Thursday at the same timeas the meeting last Tuesday $

 

与 将出现的每个 "Tuesday" 修改为 "Thursday" 相反,sed 编辑器在找到一个要修改的项目并作了修改之后继续处理下一行,而不读整行。sed 命令功能大体上类似于替换命令,这意味着它们都处理每一行中出现的第一个选定序列。为了替换出现的每一个项目,在同一行中出现多个要替换的项目的情况下, 您必须指定在全局进行该操作:

 

$ echo The tiger cubs will meet this Tuesday at the same timeas the meeting last Tuesday | sed 's/Tuesday/Thursday/g'The tiger cubs will meet this Thursday at the same timeas the meeting last Thursday$

 

请记住不管您要查找的序列是否仅包含一个字符或词组,这种对全局化的要求都是必需的。

sed 还可以用来修改记录字段分隔符。例如,以下命令将把所有的 tab 修改为空格:

 

sed 's/	/ /g' 

 

其 中,第一组斜线之间的项目是一个 tab,而第二组斜线之间的项目是一个空格。作为一条通用的规则,sed 可以用来将任意的可打印字符修改为任意其它的可打印字符。如果您想将不可打印字符修改为可打印字符—例如,铃铛修改为单词 "bell"—sed 不是适于完成这项工作的工具(但 tr 是)。

有时,您不想修改在一个文件中出现的所有指定项目。有时,您只想在满足某些条件时才作修改—例如,在与其它一些数据匹配之后才作修改。为了说明这一点,请考虑以下文本文件:

 

$ cat sample_oneone     1two     1three   1one     1two     1two     1three   1$

 

假定希望用 "2" 来替换 "1",但仅在单词 "two" 之后才作替换,而不是每一行的所有位置。通过指定在给出替换命令之前必须存在一次匹配,可以实现这一点:

 

$ sed '/two/ s/1/2/' sample_oneone     1two     2three   1one     1two     2two     2three   1$

 

现在,使其更加准确:

 

$ sed '> /two/ s/1/2/> /three/ s/1/3/' sample_oneone     1two     2three   3one     1two     2two     2three   3$

 

请 再次记住唯一改变了的是显示。如果您查看源文件,您将发现它始终保持不变。您必须将输出保存至另一个文件,以实现永久保存。值得重复的是,不对源文件作修 改实际是祸中有福—它让您能够对文件进行试验而不会造成任何实际的损害,直到让正确命令以您预期和希望的方式进行工作。

以下命令将修改后的输出保存至一个新的文件:

 

$ sed '> /two/ s/1/2/> /three/ s/1/3/' sample_one > sample_two

 

该输出文件将所有修改合并在其中,并且这些修改通常将在屏幕上显示。现在可以用 head、cat 或任意其它类似的实用工具来进行查看。

脚本文件

sed 工具允许您创建一个脚本文件,其中包含从该文件而不是在命令行进行处理的命令,并且 sed 工具通过 "-f" 选项来引用。通过创建一个脚本文件,您能够一次又一次地重复运行相同的操作,并指定比每次希望从命令行进行处理的操作详细得多的操作。

考虑以下脚本文件:

 

$ cat sedlist/two/ s/1/2//three/ s/1/3/$

 

现在可以在数据文件上使用脚本文件,获得和我们之前看到的相同的结果:

 

$ sed -f sedlist sample_oneone     1two     2three   3one     1two     2two     2three   3$

 

注意当调用 "-f" 选项时,在源文件内或命令行中不使用撇号。脚本文件,也称为源文件,对于想重复多次的操作和从命令行运行可能出错的复杂命令很有价值。编辑源文件并修改一个字符比在命令行中重新输入一条多行的项目要容易得多。

限制行

编辑器默认查看输入到流编辑器中的每一行,且默认在输入到流编辑器中的每一行上进行编辑。这可以通过在发出命令之前指定约束条件来进行修改。例如,只在此示例文件的输出的第 5 和第 6 行中用 "2" 来替换 "1",命令将为:

 

$ sed '5,6 s/1/2/' sample_oneone     1two     1three   1one     1two     2two     2three   1$

 

在这种情况下,因为要修改的行是专门指定的,所以不需要替换命令。因此,您可以灵活地根据匹配准则(可以是行号或一种匹配模式)来选择要修改哪些行(从根本上限制修改)。

禁止显示

sed 默认将来自源文件的每一行显示到屏幕上(或重定向到一个文件中),而无论该行是否受到编辑操作的影响,"-n" 参数覆盖了这一操作。"-n" 覆盖了所有的显示,并且不显示任何一行,而无论它们是否被编辑操作修改。例如:

 

$ sed -n -f sedlist sample_one$$ sed -n -f sedlist sample_one > sample_two$ cat sample_two$

 

在 第一个示例中,屏幕上不显示任何东西。在第二个示例中,不修改任何东西,因此不将任何东西写到新的文件中—它最后是空的。这不是否定了编辑的全部目的吗? 为什么这是有用的?它是有用的仅因为 "-n" 选项能够被一条显示命令 (-p) 覆盖。为了说明这一点,假定现在像下面这样对脚本文件进行了修改:

 

$ cat sedlist/two/ s/1/2/p/three/ s/1/3/p$

 

然后下面是运行它的结果:

 

$ sed -n -f sedlist sample_onetwo     2three   3two     2two     2three   3$

 

保持不变的行全部不被显示。只有受到编辑操作影响的行被显示了。在这种方式下,可以仅取出这些行,进行修改,然后把它们放到一个单独的文件中:

 

$ sed -n -f sedlist sample_one > sample_two$$ cat sample_twotwo     2three   3two     2two     2three   3$

 

利用它的另一种方法是只显示一定数量的行。例如,只显示 2-6 行,同时不做其它的编辑修改:

 

$ sed -n '2,6p' sample_onetwo     1three   1one     1two     1two     1$

 

其它所有的行被忽略,只有 2-6 行作为输出显示。这是一项出色的功能,其它任何工具都不能容易地实现。Head 将显示一个文件的顶部,而 tail 将显示一个文件的底部,但 sed 允许从任意位置取出想要的任意内容。

删除行

用一个值替换另一个值远非流编辑器可以执行的唯一功能。它还具有许多的潜在功能,在我看来第二种最常用的功能是删除。删除与替换的工作方式相同,只是它删除指定的行(如果您想要删除一个单词而不是一行,不要考虑删除,而应考虑用空的内容来替换它—s/cat//)。

该命令的语法是:

 

'{what to find} d'

 

从 sample_one 文件中删除包含 "two" 的所有行:

 

$ sed '/two/ d' sample_oneone     1three   1one     1three   1$

 

从显示屏中删除前三行,而不管它们的内容是什么:

 

$ sed '1,3 d' sample_oneone     1two     1two     1three   1$

 

只显示剩下的行,前三行不在显示屏中出现。对于流编辑器,一般当它们涉及到全局表达式时,特别是应用于删除操作时,有几点要记住:

 

  1. 上三角号 (^) 表示一行的开始,因此,如果 "two" 是该行的头三个字符,则

     

     

    sed '/^two/ d' sample_one

     

    将只删除该行。

  2. 美元符号 ($) 代表文件的结尾,或一行的结尾,因此,如果 "two" 是该行的最后三个字符,则

     

     

    sed '/two$/ d' sample_one 

     

    将只删除该行。

 

将这两者结合在一起的结果:

 

sed '/^$/ d' {filename}

 

删除文件中的所有空白行。例如,以下命令将 "1" 替换为 "2",以及将 "1" 替换为 "3",并删除文件中所有尾随的空行:

 

$ sed '/two/ s/1/2/; /three/ s/1/3/; /^$/ d' sample_oneone     1two     1three   1one     1two     2two     2three   1$

 

其通常的用途是删除一个标题。以下命令将删除文件中所有的行,从第一行直到第一个空行:

 

sed '1,/^$/ d' {filename}

 

添加和插入文本

可以结合使用 sed 和 "a" 选项将文本添加到一个文件的末尾。实现方法如下:

 

$ sed '$a\> This is where we stop\> the test' sample_oneone     1two     1three   1one     1two     1two     1three   1This is where we stopthe test$

 

在该命令中,美元符号 ($) 表示文本将被添加到文件的末尾。反斜线 (\) 是必需的,它表示将插入一个回车符。如果它们被遗漏了,则将导致一个错误,显示该命令是错乱的;在任何要输入回车的地方您必须使用反斜线。

要将这些行添加到第 4 和第 5 个位置而不是末尾,则命令变为:

 

$ sed '3a\> This is where we stop\> the test' sample_oneone     1two     1three   1This is where we stopthe testone     1two     1two     1three   1$

 

这将文本添加到第 3 行之后。和几乎所有的编辑器一样,您可以选择插入而不是添加(如果您希望这样的话)。这两者的区别是添加跟在指定的行之后,而插入从指定的行开始。当用插入来代替添加时,只需用 "i" 来代替 "a",如下所示:

 

$ sed '3i\> This is where we stop\> the test' sample_oneone     1two     1This is where we stopthe testthree   1one     1two     1two     1three   1$

 

新的文本出现在输出的中间位置,而处理通常在指定的操作执行以后继续进行。

读写文件

重定向输出的功能已经演示过了,但需要指出的是,在编辑命令运行期间可以同步地读入和写出文件。例如,执行替换,并将 1-3 行写到名称为 sample_three 的文件中:

 

$ sed '> /two/ s/1/2/> /three/ s/1/3/> 1,3 w sample_three' sample_oneone     1two     2three   3one     1two     2two     2three   3$$ cat sample_threeone     1two     2three   3$

 

由于为 w (write) 命令指定了 "1,3",所以只有指定的行被写到了新文件中。无论被写的是哪些行,所有的行都在默认输出中显示。

修改命令

除了替换项目之外,还可以将行从一个值修改为另一个值。要记住的是,替换是对字符逐个进行,而修改功能与删除类似,它影响整行:

 

$ sed '/two/ c\> We are no longer using two' sample_oneone     1We are no longer using twothree   1one     1We are no longer using twoWe are no longer using twothree   1$

 

修 改命令与替换的工作方式很相似,但在范围上要更大些—将一个项目完全替换为另一个项目,而无论字符内容或上下文。夸张一点讲,当使用替换时,只有字符 "1" 被字符 "2" 替换,而当使用修改时,原来的整行将被修改。在两种情况下,要寻找的匹配条件都仅为 "two"。

修改全部但……

对于大多数 sed 命令,详细说明各种功能要进行何种修改。利用感叹号,可以在除指定位置之外的任何地方执行修改—与默认的操作完全相反。

例如,要删除包含单词 "two" 的所有行,操作为:

 

$ sed '/two/ d' sample_oneone     1three   1one     1three   1$

 

而要删除除包含单词 "two" 的行之外的所有行,则语法变为:

 

$ sed '/two/ !d' sample_onetwo     1two     1two     1$

 

如果您有一个文件包含一系列项目,并且想对文件中的每个项目执行一个操作,那么首先对那些项目进行一次智能扫描并考虑将要做什么是很重要的。为了使事情变得更简单,您可以将 sed 与任意迭代例程(for、while、until)结合来实现这一目的。

比如说,假定您有一个名为 "animals" 的文件,其中包含以下项目:

pig
horse
elephant
cow
dog
cat

您希望运行以下例程:

 

#mcd.kshfor I in $*doecho Old McDonald had a $Iecho E-I, E-I-Odone

 

结 果将为,每一行都显示在 "Old McDonald has a" 的末尾。虽然对于这些项目的大部分这是正确的,但对于 "elephant" 项目,它有语法错误,因为结果应当为 "an elephant" 而不是 "a elephant"。利用 sed,您可以在来自 shell 文件的输出中检查这种语法错误,并通过首先创建一个命令文件来即时地更正它们:

 

#sublist/ a a/ s/ a / an // a e/ s/ a / an //a i/ s / a / an //a o/ s/ a / an //a u/ s/ a / an /

 

然后执行以下过程:

 

$ sh mcd.ksh 'cat animals' | sed -f sublist  

 

现 在,在运行了 mcd 脚本之后,sed 将在输出中搜索单个字母 a (空格,"a",空格)之后紧跟了一个元音的任意位置。如果这种位置存在,它将把该序列修改为空格,"an",空格。这样就使问题更正后才显示在屏幕上, 并确保各处的编辑人员在晚上可以更容易地入睡。结果是:

Old McDonald had a pig
E-I, E-I-O
Old McDonald had a horse
E-I, E-I-O
Old McDonald had an elephant
E-I, E-I-O
Old McDonald had a cow
E-I, E-I-O
Old McDonald had a dog
E-I, E-I-O
Old McDonald had a cat
E-I, E-I-O

提前退出

sed 默认读取整个文件,并只在到达末尾时才停止。不过,您可以使用退出命令提前停止处理。只能指定一条退出命令,而处理将一直持续直到满足调用退出命令的条件。

例如,仅在文件的前五行上执行替换,然后退出:

 

$ sed '> /two/ s/1/2/> /three/ s/1/3/> 5q' sample_oneone     1two     2three   3one     1two     2$

 

在退出命令之前的项目可以是一个行号(如上所示),或者一条查找/匹配命令:

 

$ sed '> /two/ s/1/2/> /three/ s/1/3/> /three/q' sample_oneone     1two     2three   3$

 

您 还可以使用退出命令来查看超过一定标准数目的行,并增加比 head 中的功能更强的功能。例如,head 命令允许您指定您想要查看一个文件的前多少行—默认数为 10,但可以使用从 1 到 99 的任意一个数字。如果您想查看一个文件的前 110 行,您用 head 不能实现这一目的,但用 sed 可以:

 

sed 110q filename

 

处理问题

当使用 sed 时,要记住的重要事项是它的工作方式。它的工作方式是:读入一行,在该行上执行它已知要执行的所有任务,然后继续处理下一行。每一行都受给定的每一个编辑命令的影响。

如果您的操作顺序没有十分彻底地考虑清楚,那么这可能会很麻烦。例如,假定您需要将所有的 "two" 项目修改为 "three",然后将所有的 "three" 修改为 "four":

 

$ sed '> /two/ s/two/three/> /three/ s/three/four/' sample_oneone     1four     1four   1one     1four     1four     1four   1$

 

最初读取的 "two" 被修改为 "three"。然后它满足为下一次编辑建立的准则,从而变为 "four"。最终的结果不是想要的结果—现在除了 "four" 没有别的项目了,而本来应该有 "three" 和 "four"。

当执行这种操作时,您必须非常用心地注意指定操作的方式,并按某种顺序来安排它们,使得操作之间不会互相影响。例如:

 

$ sed '> /three/ s/three/four/> /two/ s/two/three/' sample_oneone     1three     1four   1one     1three     1three     1four   1$

 

这非常有效,因为 "three" 值在 "two" 变成 "three" 之前得到修改。

标签和注释

可以在 sed 脚本文件中放置标签,这样一旦文件变得庞大,可以更容易地说明正在发生的事情。存在各种各样与这些标签相关的命令,它们包括:

 

  1. : 冒号表示一个标签名称。例如:

     

     

    :HERE

     

    以冒号开始的标签可以由 "b" 和 "t" 命令处理。

     

  2. b {label} 充当 "goto" 语句的作用,将处理发送至前面有一个冒号的标签。例如,

     

     

    b HERE

     

    将处理发送给行

     

    :HERE

     

    如果紧跟 b 之后没有指定任何标签,则处理转至脚本文件的末尾。

     

  3. t {label} 只要自上次输入行或执行一次 "t" 命令以来进行了替换操作,就转至该标签。和 "b" 一样,如果没有给定标签名,则处理转至脚本文件的末尾。

     

     

  4. # 符号作为一行的第一个字符将使整行被当作注释处理。注释行与标签不同,不能使用 b 或 t 命令来转到注释行上。

 

进一步的研究
sed 实用工具是 Linux 管理员拥有的最强大和灵活的工具之一。虽然本文覆盖了许多基础知识,但对于这一具有丰富功能的工具仅是蜻蜓点水。关于更多信息,最好的来源之一是 Dale Dougherty 和 Arnold Robbins 的著作 sed & awk,现在从 O'Reilly & Associates 出版社推出了其第二版(参加“接下来的步骤”)。该出版社还推出了一本可以随身携带的袖珍参考

Read More...

AWK 用法简介

AWK是Unix平台上一种可以对文本进行逐行处理的编程语言,它来源于3个创作者的名字:Aho、(Peter)Weinberg和(Brain)
Kernighan.与sed和grep很相似,awk是一种样式扫描与处理工具,但其功能却大大强于sed和grep。awk提供了极其强大的功能:它
几乎可以完成grep和sed所能完成的全部工作,同时,它还可以可以进行样式装入、流控制、数学运算符、进程控制语句甚至于内置的变量和函数。awk的
三位创建者已将它正式定义为:样式扫描和处理语言。

AWK的调用方式
1). 程序体直接写到AWK命令行中,适用较简单的情况
awk 'program' input-file1 input-file2 ...
2). 程序体写入文件中,用AWK命令调用该文件
awk -f program-file input-file1 input-file2 ...
3). C shell作为命令解释程序,调用AWK来执行AWK程序,写成的脚本
#!/bin/csh -f
awk '
{ print $8, "\t", $3} \
'
4). AWK作为命令解释程序,写成的脚本
#!/bin/awk -f
{ print $8, "\t", $3}
AWK的参数说明
awk [ -F re] [parameter...] ['prog'] [-f progfile][in_file...]
参数说明:
-F re: 允许awk更改其字段分隔符。
parameter: 该参数帮助为不同的变量赋值。
'prog': awk的程序语句段。必须用单引号:'和'括起,以防被shell解释。
这个程序语句段的标准形式为: 'pattern {action}'
  (1) pattern参数是匹配模式,跟Sed命令类似。
  (2) action参数总是被大括号包围,由一系统awk语句组成,各语句间用";"分隔。
        awk
pattern给定的样式匹配记录上执行其操作。 使用#作为注释符。
  (3) 可以省略pattern和action之一,但不能两者同时省略,当省略pattern时没
    有样式匹配,表示对所有行(记录)均执行操作,省略action时执行缺省的操作在标准输出上显示。
  (4) 可以包含多个pattern {action},如果一个记录满足多个pattern,其对应的action会被执行多次

-f progfile: 允许awk调用执行程序文件。progfile是一文本文件,必须符合awk语法。
in_file: awk的输入文件,awk允许对多个输入文件进行处理。
*awk不修改输入文件。如果未指定输入文件,awk将接受标准输入,并将结果显示在标准输出上。awk支持输入输出重定向。(跟SED类似)
AWK的记录(Record)与字段(Field)
awk
处理的工作与数据库的处理方式有相同之处,其相同处之一就是awk支持对记录和字段的处理,其中对字段的处理是grep和sed不能实现的。在awk中,
缺省的情况下总是将文本文件中的一行视为一个记录,而将一行中的某一部分作为记录中的一个字段。为了操作这些不同的字段,awk借用shell的方法,用
$1,$2,$3...这样的方式来顺序地表示行(记录)中的不同字段。特殊地,awk用$0表示整个行(记录)。不同的字段之间是用称作分隔符的字符分
隔开的。系统默认的分隔符是空格。awk允许在命令行中用-F
re的形式来改变这个分隔符。事实上,awk用一个内置的变量FS来记忆这个分隔符,可以在程序中进行修改。
AWK的运算,判断与赋值运算do-while语句
do
{
语句
}while(条件判断语句)
2.4for语句
for(初始表达式;终止条件;步长表达式)
{语句}

while、do-while和for语句中允许使用break, continue,exit来控制程序走向。
运算符 用途
------------------
x^y x的y次幂
x**y 同上
x%y 计算x/y的余数(求模)
x+y x加y
x-y x减y
x*y x乘y
x/y x除y
-y 负y(y的开关符号);也称一目减
++y y加1后使用y(前置加)
y++ 使用y值后加1(后缀加)
--y y减1后使用y(前置减)
y-- 使用后y减1(后缀减)
x=y 将y的值赋给x
x+=y 将x+y的值赋给x
x-=y 将x-y的值赋给x
x*=y x*y的值赋给x
x/=y 将x/y的值赋给x x%=y 将x%y的值赋给x
x^=y 将x^y的值赋给x
x**=y 将x**y的值赋给x

判断:
操作符 含义
x==y x等于y
x!=y x不等于y
x>y x大于y
x>=y x大于或等于y
x小于y
x<=y x小于或等于y?
x~re x匹配正则表达式re?
x!~re x不匹配正则表达式re?

赋值 (按优先级升序排列)
= 、+=、 -=、 *= 、/= 、 %=
||
&&
> >= < <= == != ~ !~
xy (字符串连结,'x''y'变成"xy")
+ -
* / %
++ - -
AWK的流程控制
1BEGINEND:
在awk
中两个特别的表达式,BEGIN和END,这两者都可用于pattern中,提供BEGIN和END的作用是给程序赋予初始状态和在程序结束之后执行一些
扫尾的工作。任何在BEGIN之后列出的操作(在{}内)将在awk开始扫描输入之前执行,而END之后列出的操作将在扫描完所有输入之后执行。因此,通
常用BEGIN来显示变量和预置(初始化)变量,用END来输出最终结果。 例:累计销售文件xs中的销售金额(假设销售金额在记录的第三字段):
$awk
>'BEGIN { FS=":";print "统计销售金额";total=0}
>{print $3;total=total+$3;}
>END {printf "销售金额总计:%.2f",total}' sx
>是shell提供的第二提示符,如要在shell程序awk语句和awk语言中换行,在行尾加反斜杠\
在这里,BEGIN预置了内部变量FS(字段分隔符)和自定义变量total,同时在扫描之前显示出输出行头。而END则在扫描完成后打印出总合计。
2、流程控制语句


2.1if...else语句:
if(表达式)
语句1
else
语句2
格式中"语句1"可以是多个语句,如果你为了方便awk判断也方便你自已阅读,你最好将多个语句用{}括起来。awk分枝结构允许嵌套。
2.2while语句
while(表达式
Break:中断当前正在执行的循环并跳到循环外执行下一条语句。
Continue:从当前位置跳到循环开始处执行。
Exit:当exit语句不在END中时,该命令表现得如同到了文件尾,所有模式或操作将停止,END模式中的操作被执行。而出现在END中的exit将导致程序终止。
AWK的变量awk
提供两种变量,一种是awk内置的变量,在awk程序中引用内置变量不需要使用标志符"$"awk提供的另一种变量是自定义变量。当然这种变量不能与内置
变量及其它awk保留字相同,在awk中引用自定义变量必须在它前面加上标志符"$"。与C语言不同的是,awk中不需要对变量进行初始化,awk根据其
在awk中第一次出现的形式和上下文确定其具体的数据类型。当变量类型不确定时,awk默认其为字符串类型。这里有一个技巧:如果你要让你的awk程序知
道你所使用的变量的明确类型,你应当在在程序中给它赋初值。
 
AWK中的函数
AWK的函数同样包括内置函数和用户自定义的函数。原始的awk不提供函数功能,只有在nawk或较新的awk版本中才可以增加函数。
函数的使用包含两部分:函数的定义与函数调用。其中函数定义又包括要执行的代码(函数本身)和从主程序代码传递到该函数的临时调用。
awk函数的定义方法如下:
function 函数名(参数表){
函数体
}
函数名必须是一个合法的标志符,参数表中可以不提供参数(但在调用函数时函数名后的一对括号仍然是不可缺少的),也可以提供一个或多个参数。awk的参数也是通过值来传递的。

在awk
中调用函数比较简单,其方法与C语言相似,但awk比C语言更为灵活,它不执行参数有效性检查。换句话说,在你调用函数时,可以列出比函数预计(函数定义
中规定)的多或少的参数,多余的参数会被awk所忽略,而不足的参数,awk将它们置为缺省值0或空字符串,具体置为何值,将取决于参数的使用方式。
awk函数有两种返回方式:隐式返回和显式返回。当awk执行到函数的结尾时,它自动地返回到调用程序,这是函数是隐式返回的。如果需要在结束之前退出函数,可以明确地使用返回语句提前退出。方法是在函数中使用形如:return 返回值 格式的语句。
例:下面的例子演示了函数的使用。
nawk
>'BEGIN{pageno=1;file=FILENAME
>pageno=print_header(file,pageno);span lang="EN-US">#调用函数print_header
>printf("当前页页号是:%d\n",pageno);
}

#定义函数print_header
function print_header(FileName,PageNum){
printf("%s %d\n",FileName,PageNum); >PageNum++;return PageNUm;
}
}' myfile
AWK的内置变量(预定义变量)
说明:表中v项表示第一个支持变量的工具:A=awk,N=nawk,P=POSIX awk,G=gawk
V 变量 含义 缺省值
--------------------------------------------------------
N ARGC 命令行参数个数
G ARGIND 当前被处理文件的ARGV标志符
N ARGV 命令行参数数组
G CONVFMT 数字转换格式 %.6g
P ENVIRON UNIX环境变量
N ERRNO UNIX系统错误消息
G FIELDWIDTHS 输入字段宽度的空白分隔字符串
A FILENAME 当前输入文件的名字
P FNR 当前记录数
A FS 输入字段分隔符 空格
G IGNORECASE 控制大小写敏感0(大小写敏感)
A NF 当前记录中的字段个数
A NR 已经读出的记录数
A OFMT 数字的输出格式 %.6g
A OFS 输出字段分隔符 空格
A ORS 输出的记录分隔符 新行
A RS 输入的记录他隔符 新行
N RSTART 被匹配函数匹配的字符串首
N RLENGTH 被匹配函数匹配的字符串长度
N SUBSEP 下标分隔符 "\034"

AWK的内置函数
V 函数 用途或返回值
------------------------------------------------
N gsub(reg,string,target) 每次常规表达式reg匹配时替换target中的string
N index(search,string) 返回string中search串的位置
A length(string) 求串string中的字符个数
N match(string,reg) 返回常规表达式reg匹配的string中的位置
N printf(format,variable) 格式化输出,按format提供的格式输出变量variable。
N split(string,store,delim) 根据分界符delim,分解string为store的数组元素
N sprintf(format,variable) 返回包含基于format的格式化数据,variables是要放到串中的数据
G strftime(format,timestamp) 返回format日期或时间串,timestmp是systime()函数返回的时间
N sub(reg,string,target) 第一次当常规表达式reg匹配,替换target串中的字符串
A substr(string,position,len) 返回一个以position开始len个字符的子串
P totower(string) 返回string中对应的小写字符
P toupper(string) 返回string中对应的大写字符
A atan(x,y) x的余切(弧度)
N cos(x) x的余弦(弧度)
A exp(x) e的x幂
A int(x) x的整数部分
A log(x) x的自然对数值
N rand() 0-1之间的随机数
N sin(x) x的正弦(弧度)
A sqrt(x) x的平方根
A srand(x) 初始化随机数发生器。如果忽略x,则使用system()
G system() 返回自1970年1月1日以来经过的时间(按秒计算)
一些例子:
<!--[if !supportLists]-->1.       打印11月被修改的文件的字节数
ls –lg | awk ‘$6 == “NOV” {sum += $5} END {print sum}’

2.       文件result中包含FAILED的行数
awk ‘/FAILED/ {sum += 1} END {print sum}’ result

3.       打印file文件最长行的长度
awk ‘if (length($0) > max) max = length($0)} END {print max}’ file

4.       输出file文件的行数
awk ‘END {print NR} file

NR是内置变量,表示已经读出的记录数,在END后输出即是总行数
用cat file | wc –l 命令组合可以完成同样的功能

Read More...

星期三, 十月 17, 2007

Awk学习 :通用线程:Awk 实例,第 1部分

KenShinXF 来自 developerWorks(TM) 中国网站的推荐
---------------------------------------------------------------------

标题 Awk学习 :通用线程:Awk 实例,第 1部分

Awk 是一种非常好的语言,同时有一个非常奇怪的名称。在本系列(共三篇文章)的第一篇文章中,Daniel Robbins 将使您迅速掌握 awk 编程技巧。随着本系列的进展,将讨论更高级的主题,最后将演示一个高级的真实 awk 应用程序。

了解更多

http://www-128.ibm.com/developerworks/cn/linux/shell/awk/awk-1/


IBM developerWorks 中国
IBM 为开发人员提供的资源

http://www.ibm.com/developerworks/cn

Read More...

星期日, 十月 14, 2007

wget 使用技巧

wget 是一个命令行的下载工具。对于我们这些 Linux 用户来说,几乎每天都在使用它。下面为大家介绍几个有用的 wget 小技巧,可以让你更加高效而灵活的使用 wget。



  1. $ wget -r -np -nd http://example.com/packages/

    这条命令可以下载 http://example.com 网站上 packages 目录中的所有文件。其中,-np 的作用是不遍历父目录,-nd 表示不在本机重新创建目录结构。

  2. $ wget -r -np -nd --accept=iso http://example.com/centos-5/i386/

    与上一条命令相似,但多加了一个 --accept=iso 选项,这指示 wget 仅下载 i386 目录中所有扩展名为 iso 的文件。你也可以指定多个扩展名,只需用逗号分隔即可。

  3. $ wget -i filename.txt

    此命令常用于批量下载的情形,把所有需要下载文件的地址放到 filename.txt 中,然后 wget 就会自动为你下载所有文件了。

  4. $ wget -c http://example.com/really-big-file.iso

    这里所指定的 -c 选项的作用为断点续传。

  5. $ wget -m -k (-H) http://www.example.com/

    该命令可用来镜像一个网站,wget 将对链接进行转换。如果网站中的图像是放在另外的站点,那么可以使用 -H 选项。

Read More...

星期五, 十月 12, 2007

转: Linux环境进程间通信系列:共享内存

共享内存(上)

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

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

Linux2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,本文将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。

一、内核怎样保证各个进程寻址到同一个共享内存区域的内存页面

1page cacheswap cache中页面的区分:一个被访问文件的物理页面都驻留在page cacheswap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cacheswap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

2、文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

4、对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。
注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。

5、所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
注:一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

上面涉及到了一些数据结构,围绕数据结构理解问题会容易一些。

二、mmap()及其相关系统调用

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

注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

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

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。这里不再详细介绍mmap()的参数,读者可参考mmap()手册页获得进一步的信息。

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

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

fd=open(name, flag, mode);

if(fd&~~SPECIAL_REMOVE!#~~lt;0)

...


ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。

2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可,参见范例2

3、系统调用munmap()

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

4、系统调用msync()

int msync ( void * addr , size_t len, int flags)
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

三、mmap()范例

下面将给出使用mmap()的两个范例:范例1给出两个进程通过映射普通文件实现共享内存通信;范例2给出父子进程通过匿名映射实现共享内存。系统调用mmap()有许多有趣的地方,下面是通过mmap()映射普通文件实现进程间的通信的范例,我们通过该范例来说明mmap()实现共享内存的特点及注意事项。

范例1:两个进程通过映射普通文件实现共享内存通信

范例1包含两个子程序:map_normalfile1.cmap_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1map_normalfile2。两个程序通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile1把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。

下面是两个程序代码:

/*-------------map_normalfile1.c-----------*/

#include &~~SPECIAL_REMOVE!#~~lt;sys/mman.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;sys/types.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;fcntl.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;unistd.h&~~SPECIAL_REMOVE!#~~gt;

typedef struct{

char name[4];

int age;

}people;

main(int argc, char** argv) // map a normal file as shared mem:

{

int fd,i;

people *p_map;

char temp;

fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);

lseek(fd,sizeof(people)*5-1,SEEK_SET);

write(fd,"",1);

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

close( fd );

temp = 'a';

for(i=0; i&~~SPECIAL_REMOVE!#~~lt;10; i++)

{

temp += 1;

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

( *(p_map+i) ).age = 20+i;

}

printf(" initialize over \n ")

sleep(10);

munmap( p_map, sizeof(people)*10 );

printf( "umap ok \n" );

}

/*-------------map_normalfile2.c-----------*/

#include &~~SPECIAL_REMOVE!#~~lt;sys/mman.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;sys/types.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;fcntl.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;unistd.h&~~SPECIAL_REMOVE!#~~gt;

typedef struct{

char name[4];

int age;

}people;

main(int argc, char** argv) // map a normal file as shared mem:

{

int fd,i;

people *p_map;

fd=open( argv[1],O_CREAT|O_RDWR,00777 );

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

for(i = 0;i&~~SPECIAL_REMOVE!#~~lt;10;i++)

{

printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );

}

munmap( p_map,sizeof(people)*10 );

}

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

map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10people结构,并输出读取的值,然后解除映射。

分别把两个程序编译成可执行文件map_normalfile1map_normalfile2后,在一个终端上先运行./map_normalfile2 /tmp/test_shm,程序输出结果如下:

initialize over

umap ok

map_normalfile1输出initialize over 之后,输出umap ok之前,在另一个终端上运行map_normalfile2 /tmp/test_shm,将会产生如下输出(为了节省空间,输出结果为稍作整理后的结果)

name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;

name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;

map_normalfile1 输出umap ok后,运行map_normalfile2则输出如下结果:

name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;

name: age 0; name: age 0; name: age 0; name: age 0; name: age 0;

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

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

2 可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。打开文件被截短为5people结构大小,而在map_normalfile1中初始化了10people数据结构,在恰当时候(map_normalfile1输出initialize over 之后,输出umap ok之前)调用map_normalfile2会发现map_normalfile2将输出全部10people结构的值,后面将给出详细讨论。
注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程可以对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;但是,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。

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

范例2:父子进程通过匿名映射实现共享内存

#include &~~SPECIAL_REMOVE!#~~lt;sys/mman.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;sys/types.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;fcntl.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;unistd.h&~~SPECIAL_REMOVE!#~~gt;

typedef struct{

char name[4];

int age;

}people;

main(int argc, char** argv)

{

int i;

people *p_map;

char temp;

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

if(fork() == 0)

{

sleep(2);

for(i = 0;i&~~SPECIAL_REMOVE!#~~lt;5;i++)

printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age);

(*p_map).age = 100;

munmap(p_map,sizeof(people)*10); //实际上,进程终止时,会自动解除映射。

exit();

}

temp = 'a';

for(i = 0;i&~~SPECIAL_REMOVE!#~~lt;5;i++)

{

temp += 1;

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

(*(p_map+i)).age=20+i;

}

sleep(5);

printf( "parent read: the first people,s age is %d\n",(*p_map).age );

printf("umap\n");

munmap( p_map,sizeof(people)*10 );

printf( "umap ok\n" );

}

考察程序的输出结果,体会父子进程匿名共享内存:

child read: the 1 people's age is 20

child read: the 2 people's age is 21

child read: the 3 people's age is 22

child read: the 4 people's age is 23

child read: the 5 people's age is 24

parent read: the first people,s age is 100

umap

umap ok

四、对mmap()返回地址的访问

前面对范例运行结构的讨论中已经提到,linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:


注意:文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分,一定要注意为页面大小的整数倍。下面是对进程映射地址空间的访问范例:

#include &~~SPECIAL_REMOVE!#~~lt;sys/mman.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;sys/types.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;fcntl.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;unistd.h&~~SPECIAL_REMOVE!#~~gt;

typedef struct{

char name[4];

int age;

}people;

main(int argc, char** argv)

{

int fd,i;

int pagesize,offset;

people *p_map;

pagesize = sysconf(_SC_PAGESIZE);

printf("pagesize is %d\n",pagesize);

fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);

lseek(fd,pagesize*2-100,SEEK_SET);

write(fd,"",1);

offset = 0; //此处offset = 0编译成版本1offset = pagesize编译成版本2

p_map = (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);

close(fd);

for(i = 1; i&~~SPECIAL_REMOVE!#~~lt;10; i++)

{

(*(p_map+pagesize/sizeof(people)*i-2)).age = 100;

printf("access page %d over\n",i);

(*(p_map+pagesize/sizeof(people)*i-1)).age = 100;

printf("access page %d edge over, now begin to access page %d\n",i, i+1);

(*(p_map+pagesize/sizeof(people)*i)).age = 100;

printf("access page %d over\n",i+1);

}

munmap(p_map,sizeof(people)*10);

}

如程序中所注释的那样,把程序编译成两个版本,两个版本主要体现在文件被映射部分的大小不同。文件的大小介于一个页面与两个页面之间(大小为:pagesize*2-99),版本1的被映射部分是整个文件,版本2的文件被映射部分是文件大小减去一个页面后的剩余部分,不到一个页面大小(大小为:pagesize-99)。程序中试图访问每一个页面边界,两个版本都试图在进程空间中映射pagesize*3的字节数。

版本1的输出结果如下:

pagesize is 4096

access page 1 over

access page 1 edge over, now begin to access page 2

access page 2 over

access page 2 over

access page 2 edge over, now begin to access page 3

Bus error //被映射文件在进程空间中覆盖了两个页面,此时,进程试图访问第三个页面

版本2的输出结果如下:

pagesize is 4096

access page 1 over

access page 1 edge over, now begin to access page 2

Bus error //被映射文件在进程空间中覆盖了一个页面,此时,进程试图访问第二个页面

结论:采用系统调用mmap()实现进程间通信是很方便的,在应用层上接口非常简洁。内部实现机制区涉及到了linux存储管理以及文件系统等方面的内容,可以参考一下相关重要数据结构来加深理解。在本专题的后面部分,将介绍系统v共享内存的实现。

共享内存(下)

在共享内存(上)中,主要围绕着系统调用mmap()进行讨论的,本部分将讨论系统V共享内存,并通过实验结果对比来阐述两者的异同。系统V共享内存指的是把所有共享数据放在共享内存区域(IPC shared memory region),任何想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。

系统调用mmap()通过映射一个普通文件实现共享内存。系统V则是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特殊文件系统shm中的一个文件(这是通过shmid_kernel结构联系起来的),后面还将阐述。

1、系统V共享内存原理

进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在保证shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构注同时,还将在特殊文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentryinode结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。所有这一切都是系统调用shmget完成的。

注:每一个共享内存区都有一个控制结构struct shmid_kernelshmid_kernel是共享内存区域中非常重要的一个数据结构,它是存储管理和文件系统结合起来的桥梁,定义如下:

struct shmid_kernel /* private to the kernel */

{

struct kern_ipc_perm shm_perm;

struct file * shm_file;

int id;

unsigned long shm_nattch;

unsigned long shm_segsz;

time_t shm_atim;

time_t shm_dtim;

time_t shm_ctim;

pid_t shm_cprid;

pid_t shm_lprid;

};

该结构中最重要的一个域应该是shm_file,它存储了将被映射文件的地址。每个共享内存区对象都对应特殊文件系统shm中的一个文件,一般情况下,特殊文件系统shm中的文件是不能用read()write()等方法访问的,当采取共享内存的方式把其中的文件映射到进程地址空间后,可直接采用访问内存的方式对其访问。

这里我们采用[1]中的图表给出与系统V共享内存相关数据结构:


正如消息队列和信号灯一样,内核通过数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。上图中的shm_ids.entries变量指向一个ipc_id结构数组,而每个ipc_id结构数组中有个指向kern_ipc_perm结构的指针。到这里读者应该很熟悉了,对于系统V共享内存区来说,kern_ipc_perm的宿主是shmid_kernel结构,shmid_kernel是用来描述一个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在shmid_kernel结构的file类型指针shm_file指向文件系统shm中相应的文件,这样,共享内存区域就与shm文件系统中的文件对应起来。

在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已经创建了文件系统shm中的一个同名文件与共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理与mmap()大同小异。

2、系统V共享内存API

对于系统V共享内存,主要有以下几个APIshmget()shmat()shmdt()shmctl()

#include &~~SPECIAL_REMOVE!#~~lt;sys/ipc.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;sys/shm.h&~~SPECIAL_REMOVE!#~~gt;

shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享区域进行访问操作。shmdt()调用用来解除进程对共享内存区域的映射。shmctl实现对共享内存区域的控制操作。这里我们不对这些系统调用作具体的介绍,读者可参考相应的手册页面,后面的范例中将给出它们的调用方法。

注:shmget的内部实现包含了许多重要的系统V共享内存机制;shmat在把共享内存区域映射到进程空间时,并不真正改变进程的页表。当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核再根据相应的存储管理机制为共享内存映射区域分配相应的页表。

3、系统V共享内存限制

/proc/sys/kernel/目录下,记录着系统V共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,可以手工对其调整,但不推荐这样做。

[2]中,给出了这些限制的测试方法,不再赘述。

4、系统V共享内存范例

本部分将给出系统V共享内存API的使用方法,并对比分析系统V共享内存机制与mmap()映射普通文件实现共享内存之间的差异,首先给出两个进程通过系统V共享内存通信的范例:

/***** testwrite.c *******/

#include &~~SPECIAL_REMOVE!#~~lt;sys/ipc.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;sys/shm.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;sys/types.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;unistd.h&~~SPECIAL_REMOVE!#~~gt;

typedef struct{

char name[4];

int age;

} people;

main(int argc, char** argv)

{

int shm_id,i;

key_t key;

char temp;

people *p_map;

char* name = "/dev/shm/myshm2";

key = ftok(name,0);

if(key==-1)

perror("ftok error");

shm_id=shmget(key,4096,IPC_CREAT);

if(shm_id==-1)

{

perror("shmget error");

return;

}

p_map=(people*)shmat(shm_id,NULL,0);

temp='a';

for(i = 0;i&~~SPECIAL_REMOVE!#~~lt;10;i++)

{

temp+=1;

memcpy((*(p_map+i)).name,&temp,1);

(*(p_map+i)).age=20+i;

}

if(shmdt(p_map)==-1)

perror(" detach error ");

}

/********** testread.c ************/

#include &~~SPECIAL_REMOVE!#~~lt;sys/ipc.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;sys/shm.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;sys/types.h&~~SPECIAL_REMOVE!#~~gt;

#include &~~SPECIAL_REMOVE!#~~lt;unistd.h&~~SPECIAL_REMOVE!#~~gt;

typedef struct{

char name[4];

int age;

} people;

main(int argc, char** argv)

{

int shm_id,i;

key_t key;

people *p_map;

char* name = "/dev/shm/myshm2";

key = ftok(name,0);

if(key == -1)

perror("ftok error");

shm_id = shmget(key,4096,IPC_CREAT);

if(shm_id == -1)

{

perror("shmget error");

return;

}

p_map = (people*)shmat(shm_id,NULL,0);

for(i = 0;i&~~SPECIAL_REMOVE!#~~lt;10;i++)

{

printf( "name:%s\n",(*(p_map+i)).name );

printf( "age %d\n",(*(p_map+i)).age );

}

if(shmdt(p_map) == -1)

perror(" detach error ");

}

testwrite.c创建一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据。分别把两个程序编译为testwritetestread,先后执行./testwrite./testread ./testread输出结果如下:

name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;

name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;

通过对试验结果分析,对比系统Vmmap()映射普通文件实现共享内存通信,可以得出如下结论:

1 系统V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。注:前面讲到,系统V共享内存机制实际是通过映射特殊文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统重新引导后,所有的内容都丢失。

2 系统V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。

3 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。而通过系统V共享内存实现通信的进程则不然。注:这里没有给出shmctl的使用范例,原理与消息队列大同小异。

Read More...