centos6.4学习笔记

1、shell中""与“空格”的区别

testing=$(netstat -tuln | grep ":80 ")   # 侦测看 port 80 在否?
if [ "$testing" != "" ]; then
	echo "WWW is running in your system."
fi

上例中使用的是“”,中间没有空格,表明是空字符,运行正确。如果改用“空格”的话,表明是否检测到的为空格这个字符,与原意相悖。

2、中括号[和]的使用细节

    (1)在中括号[]内的每个组件都必须用空格键来分隔;

    (2)在中括号内的变量,最好都以双引号括起来;

    (3)在中括号内的常量,最好都以单引号或者双引号括起来。

3、变量关键字$的注意事项

    (1)假如需要在变量name的后面加接yes,则正确的使用方法为#2和#3.

name=$nameyes                          #1
name="$name"yes                       #2
name=${name}yes                       #3

    (2)变量关键字$应用于算术表达式中的注意事项

    变量关键字$应用于算术表达式时,$与相邻的括号(和)最好紧密相连,中间不留空格。  

比较下列代码

while [ "$i" != "100" ]
do
        i=$(($i+1))                            #1
        s=$(($s+$i))                         #2
done
#########################################
while [ "$i" != "100" ]
do
        i=$ ( ( $i + 1 ) )                         #3
        s=$ ( ( $s + $i ) )                      #4
done
#1和#2中$与相邻的括号(和)中间没有空格,而#3和#4中存在空格。 vim at centos6.2下#1和#2通过,而#3和#4编译出错。

4、限定连续RE字符范围{}

我们可以利用.和*来设置0个到无限多个重复字符,利用{和}来限制一个范围区间内的重复字符数。但是因为{和}的符号在shell中有特殊的意义,因此,我们必须要使用转义字符\来设置。下例为找到两个o的字符串。

grep -n 'o\{2\}' regular_express.txt

5、符号^在字符集合符号(中括号[])之内与之外的区别

在中括号[]之内代表”反向选择“;在之外代表”定位在行首“。

下例为查找开头不是英文字母的行。

grep -n '^[^a-zA-Z]' regular_express.txt


6、切换用户的陷阱

单纯使用‘su [username]’切换用户成为root的身份,读取的变量设置方式为non-login shell的方式,这种方式下很多原本的变量不会被改变(如PATH、MAIL)。如果要完整地切换到新用户的环境,必须使用“su -l username”的方式,才会连同PATH/MAIL等变量都转换成新用户的环境。如果仅仅想执行一次root的命令,可以使用“su - -c "命令串"”的方式处理。


7、vim的高级配置

参看:http://www.cnblogs.com/ma6174/archive/2011/12/10/2283393.html


/*

服务器架构篇

*/

1、在虚拟机上采用固定IP上网方式时,必须把默认的网络连接方式(NAT或HOSTONLY)改成为“桥接”方式。否则机器会被隔离而无法ping通。

手动设置IP的重点设置项为:

DEVICE=
HWADDR=
BOOTPROTO=static
ONBOOT=yes

IPADDR=
NETWORK=
BROADCAST=
NETMASK=
GATEWAY=

DNS1=
DNS2=


2、Linux修改eth2到eth0   摘抄自:http://blog.fbbin.com/archives/1305

Linux是用/etc/network/interfaces这个配置文件来载入网卡的。默认配置中,它指示Linux会自动寻找eth0网卡,并自动分配IP。但是,当虚拟网卡的MAC地址更改后,网卡在Linux中不是eth0了,而是eth1或其它值。因此才会找不到。而现有系统中,有哪些网卡是可用的,可以通过下面的命令查看:
ifconfig -a
使用这个命令后确实,发现每刷新一次mac地址,就生成一个新的eth。
在Linux中,还有一个配置文件:/etc/udev/rules.d/70-persistent-net.rules,打开这个配置文件后,会发现里面列出了所有Linux找到的网卡,包括历史的。因此,解决无法加载网卡的问题就有两个解决办法:
1.修改interfaces配置文件,使其使用最新的eth网卡
2.删除.rules文件,让linux重新将网卡认为是eth0


3、多网卡配置:

cat /etc/sysconfig/network-scripts/ifcfg-eth0

DEVICE=eth0
BOOTPROTO=static
IPADDR=192.168.1.2
NETMASK=255.255.255.0
ONBOOT=yes

cat /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth1
BOOTPROTO=static
IPADDR=222.157.4.82
NETMASK=255.255.255.128
ONBOOT=yes

vi   /etc/rc.d/rc.local   
route add -net 192.168.0.0/24 gw 192.168.0.254 dev eth0
route add -net 222.157.0.0/16 gw 222.157.66.1 dev eth1  


/*

UNIX环境高级编程(第二版)篇

*/

1、关于从265页后,执行书上含有signal函数的代码显示结果与书上结果又差异的解释:

因为从265页后的signal函数都是采用的程序清单10-12的代码,此代码对除SIGALRM外的所有信号中断调用了重启动。所以在代码中加入程序清单10-12即可。



发现一位总结得很好的博主,多学习:http://blog.163.com/ljf_gzhu/blog/#m=0

以下是Jim Liang的Linux编程的日志,偷偷的转到这里以供以后查阅。

0. 前言  

2011-07-30 16:00:42|  分类: 《Linux编程从入|字号 订阅

        两年前(2009年),本人刚开始接触Linux不久,公司便接到一个颇具挑战性的太阳能热水器控制系统项目,由于新项目的需要本人购买了下面这本入门教程《Linux编程从入门到精通》-宫虎波 编著:
0. 前言 - Jim - Jim Liang
 
        一本朴素而整洁的入门教程,正是它使得我们的项目开发大大地加快了进度,凭着本人在Windows平台上多年的编程经验,因此在项目开发初期便直奔主题:直接从第四篇《进程间通信(IPC)及网络编程》开始,在最短时间内进一步确立了新项目的软件架构。现在该项目已经接近尾声,趁着有少少的时间间隙,本人将继续推行个人长期以来的学习方针:全面系统地学习、稳步前进!现在就从第一页开始认认真真地将它学习一遍,并做好笔记,方便日后复习也希望能为各位Linux爱好者提供一点点参考。
       对于入门者来说,这的确是一本不错的参考书。启程了......
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

1. shell编程  

2011-08-01 20:27:01|  分类: 《Linux编程从入|字号 订阅

1.1 shell简介
        shell的中文含义是“外壳”,这个名称表示与系统内核相对应的外壳应用程序。Linux系统下有多种shell,如常见的Bourne Again shell (Bash)、C shell (Csh)、Korn shell等。Bash是Linux系统下标准的shell。当系统创建一个新的用户时,如果不特别指定,其采用的shell默认就是Bash。

1.2 shell环境变量
        所谓环境变量,就是包含了系统和当前登录用户的一些环境信息(或者成为配置信息)的一些字符串。也可以看作是一些在shell环境中定义的全局变量。操作系统本身会包含一部分环境变量,同时每个用户也可以设置自己不同的环境变量。

命令:创建/设置环境变量
export 环境变量名=环境变量值

命令:查看环境变量
echo $环境变量名

注意:
环境变量的命名与使用必须遵守一定的规则,如下:
1)变量名与变量值必须以等号“=”连接,等号两边不能有空格符;
2)变量名只能是英文字母和数字,但不能用数字作为开头字符;
3)变量的引用需要通过符号“$”作为前导符;
4)在使用引号时,如果需要在引号中间引用环境变量,则应该使用双引号; (??这点不明白,以后有空测试)

1.3 管道与重定向
        所谓管道,就是将一个命令的输出作为另一个命令的输入。如:
         ls|wc -l
其中的“|”就是管道符号。通过管道,将ls命令的输出作为wc命令的输入。
        除了支持管道操作外,Linux还支持输入/输出的重定向。默认情况下,shell的输入是键盘,输出是屏幕。通过重定向技术,可以将输入/输出重新定向到指定的目标,如某个文件或打印机等。

1.3.1 Linux的输出重定向有两个操作符:
1)">": 如果重定向到文件,则目标文件被覆盖;
2)“>>": 如果重定向到文件,则将在目标文件的尾部追加;

1.3.2 Linux的输入重定向也包含两个操作符
1)“<"
2)”<<": 也称为here文档操作符。它通知shell当前运行的命令的输入来自命令行。“<<"操作符后面的输入用一个分隔符开始,用同样的分隔符结束。两个分隔符间的内容就是要执行的命令的输入。

例 1-1:统计一段文字单词的数量
wc<<flag
This is a test program!
This is the second line.
flag

注意:
在使用“<<”操作符时,最后的分隔符要以一个新行开始。

1.4 shell变量的使用
        shell环境中的变量有多种类型,一是环境变量,而是用户自定义变量,三是系统全局变量。

1.4.1 shell变量的定义
        shell是一种解析执行的脚本语言,所以用户自定义变量的使用不需要事先定义。通常情况下,使用变量时直接对变量赋初值,如下:
变量名=变量初值(注意等号两边不能有空格)

1.4.2 shell变量的命名规则
1)首个字符必须为字母(a~z、A~Z);
2)中间不能有空格,可以使用下划线“_";
3)不能使用标点符号;
4)不能使用shell关键字(可执行help命令查看保留关键字)。

1.4.3 shell全局变量
        shell中,主要的全局变量包括:
1)$#:执行shell脚本时的命令行参数个数,不包括脚本本身;
2)$?:执行上一条shell命令的返回值;
3)位置变量:位置变量类似于C语言编程中的命令行参数,它代表了执行shell程序时的命令行参数。要在shell脚本中引用位置变量,通过$0,$1,$2,..的方式进行。其中,$0代表shell命令本身,$1代表第一个参数,$2代表第二个参数,依此类推。
4)$*:包含了全部的命令行参数

例1-2:创建一个shell脚本sum,计算并输出两个输入数据的和,代码如下:
sum=`expr $1 + $2`
echo $1+$2=$sum
【测试】在shell下执行 :./sum 1 2,结果为:1+2=3

1.5 shell运算符

1.5.1 expr命令
        在进行shell编程时,如果需要进行科学计算,则需要用到expr命令。expr命令支持的运算符如下:
1)+:加法
2)-:减法
3)*:乘法
4)/:除法
5)%:求余
6)>, >=, <, <=, =, !=:大于,大于等于,小于,小于等于,等于,不等于
7)&, |:与,或
8)::字符串比较,如果两个字符串相等,则返回字符串的长度,否则返回0

1.5.2 test命令
        test命令主要用于进行逻辑测试,可以用test命令进行测试的情况包括以下4种:
1)整数测试运算
test int1 -eq int2:判断两个整数是否相等
test int1 -ne int2:判断两个整数是否不相等
test int1 -gt int2:判断整数1是否大于整数2
test int1 -ge int2:判断整数1是否大于等于整数2
test int1 -lt int2:判断整数1是否小于整数2
test int1 -le int2:判断整数1是否小于等于整数2
2)字符串测试运算
test -z string:判断字符串的长度是否为0(为空),为空返回真,非空返回假
test -n string:判断字符串的长度是否不为0(非空),非空返回真,为空返回假
test str1=str2:判断两个字符串是否相等
test str1!=str2:判断两个字符串是否不等
3)文件测试运算
test -r filename:判断用户对文件是否有读权限
test -w filename:判断用户对文件是否有写权限
test -x filename:判断用户对文件是否有可执行权限
test -f filename:判断文件是否为普通文件
test -d filename:判断文件是否为目录
test -c filename:判断文件是否为字符设备文件
test -b filename:判断文件是否为块设备文件
test -s filename:判断文件是否大小不为零
test -t fd:判断与文件描述符(默认值为1)相关的设备是否是一个终端设备
4)逻辑运算
test 表达式1 -a 表达式2:与(and)逻辑运算。如果两个表达式同时为真则返回真,否则返回假
test 表达式1 -o 表达式2:或(or)逻辑运算。只要两个表达式中有一个为真则返回真,否则返回假

1.6 选择结构程序设计
        在shell程序设计中,用于选择结构编程的有两种表达式。分别如下:

1.6.1 if表达式
if [ 条件表达式1 ]
then
    if [ 条件表达式2 ]
    then
    ...
    else
    ...
    fi
    命令串
else
    命令串
fi


注意:
在"["的前面与后面均要有空格,在“]"的前面也要有空格!

1.6.2 case表达式
case string in
str1)
命令串1;;
str2)
命令串2;;
*)
默认处理命令串;;
esac

注意:
在case语句的每个分支末尾必须添加两个分号 ";;" !

例 1-3:创建一个shell脚本weekday,输入数字,输出该数字对应的星期几,代码如下:
#判断命令行参数个数是否为1
if [ $# != 1 ]
#如果没有命令行参数,则提示用法后退出
then
    echo "Usage:$0 n (n>=0 and n<=7)"
    exit
fi
#用case表达式判断输入的数字
case $1 in
0)
    echo Sunday
    ;;
1)
    echo Monday
    ;;
2)
    echo Tuesday
    ;;
3)
    echo Wednesday
    ;;
4)
    echo Thursday
    ;;
5)
    echo Friday
    ;;
6)
    echo Saturday
    ;;
*)
    echo invalidate
    ;;
esac

【测试】在shell下执行:./weekday 0,结果为:Sunday

1.7 循环结构程序设计

1.7.1 for循环

1)形式1:
for var in list
do
命令串
done


参数说明:
【list】:列表,可以是由空格分隔的变量(如$a $b等)或者是值(如1 2 3等)。对于list中的每一项都将循环一次。
【var】:每次循环的值。每次循环时都取list中的对应项放在var中,可以在命令串中通过$var的方式引用。

2)形式2:
for var
do
命令串
done


说明:
        这种形式没有显示给出list项,此时shell将采用命令行参数作为list项。

例1-4:创建一个shell脚本computesum,从命令行输入多个数字参数,输出全部数字的和。代码如下:
#定义求和结果存储的变量,并初始化为0
sum=0
#使用for循环的第二种形式
for arg
do
    #判断是否为数字
    if [ `expr $arg \>= 0`=1 ] #???此句不明白
    then
        #累加命令行参数至sum变量
        sum=`expr $sum + $arg`
    else
        echo parameter error!
    fi
done
#输出求和结果
echo sum=$sum

【测试】在shell下执行:./computesum 1 2 3 4 5 6 -1,结果为:20

1.7.2 while循环
while [ 条件表达式 ]
do
命令串
done

注意:
在"["的前面与后面均要有空格,在“]"的前面也要有空格!

例1-5:创建一个shell脚本testcmdline,从命令行输入多个参数,使用while枚举全部。代码如下:
#定义变量,用于存储当前命令行参数的序号
n=1
#while循环,测试$*是否为空。
#$*包含了全部的命令行参数
while [ -n "$*" ]
do
    #显示每个参数的信息
    echo 第$n个参数=$1
    #序号变量加1
    n=`expr $n + 1`
    #命令行参数左移一个
    shift
done

【测试】在shell下执行:./testcmdline a b c d e f,结果为:
第1个参数=a
第2个参数=b
第3个参数=c
第4个参数=d
第5个参数=e
第6个参数=f

1.7.3 until循环
until [ 条件表达式 ]
do
命令串
done

注意:
1)在"["的前面与后面均要有空格,在“]"的前面也要有空格!
2)until循环和while循环的结构基本相同,但是until是判断条件表达式为假时才继续循环!

例1-6:用until循环实现例1-5,代码如下:
#定义变量,用于存储当前命令行参数的序号
n=1
#while循环,测试$*是否为空。
#$*包含了全部的命令行参数
until [ -z "$*" ]
do
    #显示每个参数的信息
    echo 第$n个参数=$1
    #序号变量加1
    n=`expr $n + 1`
    #命令行参数左移一个
    shift
done


1.8 shell函数
        shell函数有两种定义形式,分别为:
形式1:
函数名()
{
    ...
    return
}

形式2:
function 函数名()
{
    ...
    return
}


说明:
1)在函数中可以通过位置变量$n的方式对参数进行引用;
2)函数可以有返回值,可以通过查询全局变量$?的值获取函数的返回值;
3)函数可以使用ruturn语句返回也可以忽略,根据具体应用而定。

例1-7:创建一个脚本fact,从命令行输入一个数字,计算该数字的阶乘并输出。代码如下:

#计算阶乘的函数,函数中使用了递归调用。
#函数的两个参数分别为$1=本次阶乘因子,$2=上次阶乘结果。
fact()
{
    #如果阶乘因子为0,表明阶乘计算完成,输出结果返回
    if [ $1 = 0 ]
    then
        echo result=$2
        return
    fi
    #将当前的阶乘因子与上一次递归计算所得的阶乘结果相乘,并将结果存放于变量var中
    var=`expr $2 \* $1`
    #递归调用
    fact `expr $1 \- 1` $var
}
#调用函数fact计算阶乘
#第一个参数为命令行输入的参数,第二个参数为初始递归结果,强制为1
fact $1 1

【测试】 在shell下执行:./fact 5,结果为:result=120

1.9 综合实例


1.9.1 实例需求

        用户在进行Linux系统管理的过程中,经常需要用到查看进程的信息、用户的信息等常用的功能。本例针对这一需求,使用shell编程实现基本的系统管理功能。通过本程序,可以按照要求实现查看进程信息、查看当前登录系统的用户信息等功能。程序主界面颜色可以更改,也可以显示帮助信息。
1.9.2 系统设计
        根据需求,首先考虑到界面的美观性,将主界面设计为菜单形式。个系统管理功能作为其中的一个菜单项。当用户选择某项功能后,则执行相应的代码段完成功能并输出。输出结束后再返回主菜单,供用户继续选择。流程图如图1-1所示。

1. shell编程 - Jim - Jim Liang
 图1-1 程序流程图

1.9.3 程序代码
        按照上述设计思想,程序可以划分为几个模块:界面修改模块、主菜单模块、查看进程信息模块、查看用户信息模块和帮助模块。程序代码如下:

#############################################################
#界面修改模块:color函数

#根据输入颜色信息修改界面颜色
color()
{
    case $1 in
    black_green) #黑底绿字
        echo -e "\033[40;32m"
        ;;
    black_yellow) #黑底黄字
        echo -e "\033[40;33m"
        ;;
    black_white) #黑底白字
        echo -e "\033[40;37m"
        ;;
    black_blue) #黑底蓝字
        echo -e "\033[40;34m"
        ;;
    *) #默认为黑底白字
        echo -e "\033[30;37m"
        ;;
    esac
}

#############################################################
#主菜单模块

#清屏
echo -e "\033[2J"
#设置信号处理
trap "" 1 2 3
#日期信息
date=`date +%d/%m/%y`
#机器名信息
host=`hostname`
#当前用户信息
user=`whoami`

#循环显示主菜单
while :
do
    #显示主菜单,menu作为cat命令的结束符
    cat <<menu
    ---------------------------------------------------
        $user    $host    $date
    ---------------------------------------------------
            1:改变字体颜色
            2:查看进程信息
            3:查看用户信息
            h:帮助
            q:退出
    ---------------------------------------------------
menu
#cat命令结束
    
    #提示用户输入选项
    echo -e -n "\t请输入您的选择【1,2,3,h,q】:"
    #读取用户输入的选择项
    read choice
    #判断choice值
    case $choice in
    1) #用户输入1
        #while循环,接收用户输入的颜色选择
        while :
        do
    #显示颜色选择菜单
    cat <<menu_color
    ---------------------------------------------------
        $user    $host    $date
    ---------------------------------------------------
        1:黑绿    2:黑黄    3:黑白    4:黑蓝    0:返回
    ---------------------------------------------------
menu_color
#cat命令结束
            #提示用户输入颜色选项
            echo -e -n "\t请输入选择的颜色【1,2,3,4,0】:"
            #读取用户的输入
            read choice_color
            #判断用户输入的颜色选项
            case $choice_color in
            1) #用户输入1
                #调用color函数设置颜色
                color black_green
                ;;
            2) #用户输入2
                #调用color函数设置颜色
                color black_yellow
                ;;
            3) #用户输入3
                #调用color函数设置颜色
                color black_white
                ;;
            4) #用户输入4
                #调用color函数设置颜色
                color black_blue
                ;;
            0) #用户输入0
                #退出当前菜单
                break
                ;;
            *) #用户输入其它任意值
                #输入错误,清屏继续等待输入
                echo -e "\033[2J"
                echo -e "\tInput error! Please input again!"
                continue
                ;;
            #case结束
            esac
            
            clear
        #while结束
        done
        ;;
        
#############################################################
#查看进程信息模块

    2) #用户输入2
        #调用ps命令查看进程信息
        ps aux|sort -m|head -10
        ;;
        
#############################################################
#查看用户信息模块

    3) #用户输入3
        #调用who命令查看用户信息
        who
        ;;
        
#############################################################
#帮助模块
        
    H|h) #用户输入H或h
        #调用cat显示帮助信息
        cat <<menu_help
        选择“改变字体颜色“可以修改当前界面的字体颜色
        选择“查看进程信息“可以获取当前占用资源最高的前10个进程
        选择“查看用户信息“可以获取当前登录系统的用户信息
menu_help
#cat命令结束
        ;;
        
    Q|q) #用户输入Q或q
        #推出主程序
        exit 0
        ;;
    *) #用户输入其他值
        #输入错误,清屏继续等待输入
        echo -e "\033[2J"
        echo -e "\tInput error! Please input again!"
        continue
        ;;
    #case语句结束
    esac
    #执行完命令后的提示
    echo -e -n "\t按任意键继续..."
    read anykey
    clear
#while循环结束
done
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

2. gcc编译器  

2011-09-20 22:15:58|  分类: 《Linux编程从入|字号 订阅

2.1  gcc常用命令行参数
1)-c:只进行编译,不进行链接,输出.o文件。
2)-S:输出汇编代码文件。
3)-o:链接生成可执行文件。如果未指定生成的可执行文件名,将使用缺省名a.out代替。
4)-g:产生符号调试工具(gdb)所必要的符号信息。
5)-O:在编译链接过程中进行优化。 主要有O0~3及Os。
6)-Idirname:将dirname所指的目录加入到“头文件搜索目录列表”中。
7)-Ldirname:将dirname所指的目录加入到“函数库搜索目录列表”中。
8)-lname:在链接过程中加载名字为“libname.a”或“libname.so”的函数库。
9)-Dname:给所编译的文件定义一个宏,其名字为name,值为1。


2.2 条件编译指令

        条件编译指令包括 #ifdef、#ifndef、#else、#elif、#endif 等。通过条件编译指令,程序员可以在源程序中通过定义不同的宏来决定编译器对哪些代码进行处理。编译器将根据宏定义的配置信息,将那些不必要的代码过滤掉。

例 2-1:下面程序实现了条件编译代码,在编译程序时可以给编译器传递-D参数,这样就能方便地实现调试模式,且可以方便地传递多个-D参数。
代码如下,main.c:
#include <stdio.h>
void main()
{
#ifdef DEBUG
    printf("test1: debug info!\n");
#else
    printf("test2: release info!\n");
#endif

#ifdef DBG1
    printf("test DBG1\n");
#endif
}
1)编译命令1,调试模式:gcc -DDEBUG main.c -o test
执行:./test
输出为:"test1: debug info!"
2)编译命令2,调试模式:gcc -DDEBUG -DDBG1 main.c -o test
执行:./test
输出为:
"test1: debug info!"
"test DBG1"
3)编译命令3,发布模式:gcc main.c -o test
执行:./test
输出为:"test2: release info!"

2.3 用gcc生成动态链接库
2.3.1 生成动态库
1)编译
gcc -fPIC -c xxx1.c
gcc -fPIC -c xxx2.c
说明:选项-fPIC的作用是通知编译器产生可重定位的目标代码。
2)链接
gcc -shared -o libxxx.so xxx1.o xxx2.o
3)补充:上面生成动态库的步骤也可以简化为一条命令,如下:
gcc -shared -fPIC -o libxxx.so xxx1.c xxx2.c

2.3.2 使用动态库
1)方法一,设置环境变量LD_LIBRARY_PATH,具体如下:
        在修改/etc/profile文件,在末尾添加  export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:pathname
2)方法二,通过系统调用函数dlopen、dlsym、dlclose等实现。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

3. make工具  

2011-09-24 14:23:32|  分类: 《Linux编程从入|字号 订阅

        make工具最主要也是最基本的功能就是通过makefile文件来描述源程序之间的相互关系并自动维护编译工作。在Linux系统下默认使用“Makefile”作为makefile文件。如果要使用其他文件作为makefile,则可以在使用make命令时指定-f选项使用不同的makefile文件。

3.1 makefile的基本语法规则(显式规则)
语法如下:
Targets: Prerequisites
<Tab>Command

说明:
1)Targets:目标,通常为文件名,也可以为自定义名称。可以使用通配符。如果目标是多个文件,则以空格分开。
2)Prerequisites:依赖目标,通常为也文件名。如果其中的某个文件要比目标文件新,那么目标就被认为是“过时的”,是需要重新生成的。
3)Commands:命令行,如果其不与Prerequisites在一行,那么必须以<Tab>键开头,如果和Prerequisites在一行,那么可以用分号作为分隔。
4)如果Prerequisites或Commands太长,可以使用反斜杠“\”作为换行符。

3.2 隐式规则
参考:http://hi.baidu.com/h2hk2k/blog/item/0e2bd21024e0b6cfa6ef3f75.html

3.3 makefile变量
        在makefile中定义的变量,就像C语言中的宏一样,它代表了一个文本字串,在makefile中执行的时候会自动原模原样地展开其在所使用的位置。与C语言所不同的是,用户可以在makefile中改变其值。在makefile中,变量可以使用在“目标”、“依赖目标”、“命令”或者makefile的其他部分中。
3.3.1 自定义变量命名规则
        变量的名字可以包括字符(大小写敏感)、数字和下划线,可以是数字开头。
3.3.2 变量的使用
        变量在声明时需要给予初值,而在使用时,需要在变量名前加上“$”,但是如果变量名的长度超过一个字符,在引用时就必须添加 圆括号“()”或是大括号“{}”把变量括起来。如果要使用真实的“$”字符,那么需用“$$”来表示。
3.3.3 makefile预定义变量

1)$@:表示规则中的目标文件集。
2)$+:所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件。
3)$^:所有的依赖文件,以空格分开,不包含重复的依赖文件。
4)$?:所有比目标新的依赖目标的集合。
5)$<:第一个依赖文件的名称。
6)$*:不包含扩展名的目标文件名称。

3.4 环境变量
        make工具支持环境变量,通过设置环境变量的值,可以改变make的默认动作。
3.4.1 VPATH环境变量
        在一些大的工程中,有大量的源文件,我们通常的做法是把许多的源文件分类,并存放在不同的目录中。所以,当make需要去寻找文件的依赖关系时,可以在文件前加上路径,但最好的方法是把一个路径告诉make,让它去自动查找。环境变量VPATH就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,make就会在当前目录找不到的情况下,到所指定的目录中,按照指定的顺序进行查找。例如:
VPATH=/home/include/headers1:/home/include/headers2
如果存在多个目录,则目录间由冒号分隔。
3.4.2 CFLAGS环境变量
        指定了C语言编译器参数。
3.4.3 LDFLAGS环境变量
        指定了C语言链接器参数。

3.5 通配符
        make支持3个通配符:“*”、“?”、“[...]”。另外,波浪号“~”字符在文件名中也有比较特殊的用途。例如“~/test”表示当前用户的$HOME目录下的test目录。

3.6 使用条件判断
        使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或是比较变量和常量的值。语法如下:
<conditional-directive>
<text-if-true>
endif

或者:
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif

其中,<conditional-directive>代表条件关键字,共有4个:ifeq、ifneq、ifdef、ifndef。
3.6.1 ifeq语法
ifeq (<arg1>,<arg2>)
ifeq '<arg1>' '<arg2>'
ifeq "<arg1>" "<arg2>"
ifeq "<arg1>" '<arg2>'
ifeq '<arg1>' "<arg2>"

ifeq用于比较参数arg1和arg2的值是否相同。如果相同则返回真,否则返回假。
3.6.2 ifneq语法
ifneq (<arg1>,<arg2>)
ifneq '<arg1>' '<arg2>'
ifneq "<arg1>" "<arg2>"
ifneq "<arg1>" '<arg2>'
ifneq '<arg1>' "<arg2>"

ifneq用于比较参数arg1和arg2是否不同,如果不同则返回真,否则返回假。
3.6.3 ifdef语法
ifdef <variable-name>

如果变量<variable-name>的值非空,那么表达式为真;否则,表达式为假。
3.6.4 ifndef语法
ifndef <variable-name>
如果变量<variable-name>的值为空,那么表达式为真;否则,表达式为假。

注意:使用条件判断时,不应该加前缀<Tab>键,否则,将被解释为命令。

3.7 在makefile中使用函数
        makefile支持函数调用。通过使用函数,可以使makefile的使用更为灵活。函数调用后的返回值可以在makefile中获取,用户可以根据返回值进行下一步的处理。使用函数的语法为:
$(<函数名> <参数1>,<参数2> <...>)
参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。
常用的makefile函数包括以下几个。
3.7.1 字符串替换函数 substr
$(substr <from>,<to>,<text>)
功能:将字符串text中from的内容替换为to。
3.7.2 去空格函数 strip
$(strip <string>)
功能:将字符串string中的前后空格去掉
3.7.3 查找字符串函数 findstring
$(findstring <find>,<in>)
功能:在字符串in中查找与find指定内容匹配的字串。如果查找成功,则返回find指定的串,否则返回空串。
3.7.4 排序函数 sort
$(sort <list>)

其中,list是待排序的字符串集合,返回值是经过排序后的字符串集合。需要注意的是,sort函数将对重复的串进行过滤,只保留一次。
3.7.5 取目录函数 dir
$(dir <path>)
功能:从路径path取出目录部分并返回
3.7.6 取前缀函数 basename
$(basename <filename>)
功能:从文件名filename中取出,去掉后缀(.及后面的部分)后的内容并返回。
3.7.7 变量来源函数 orgin
$(orgin <var>)
功能:返回变量的来源。可能的来源包括以下几点:
1)Undefined:变量未定义过。
2)Defualt:默认的变量定义。
3)File:在makefile中定义的变量
4)Command line:在命令行中定义的变量,如在make的命令行中定义的变量等。

3.8 make命令的工作过程
        make命令的工作是按照一定规则进行的。通常情况下,一个make命令的工作过程如下:
1)读入makefile,make是在当前目录下查找makefile或者Makefile的。如果用户通过-f指定其他makefile名称,则按照用户指定的文件去查找。
2)初始化makefile文件中的变量,如果makefile包含了其他的makefile,则加载之。
3)推导隐含规则,并分析所有规则,检查各种要素间的依赖关系,为所有的目标文件创建依赖关系链。
4)根据依赖关系,检查哪些依赖项发生了变更,分析哪些文件需要重新生成。
5)执行生成命令。

3.9 在makefile中使用伪目标
        根据GNU规范的要求,建议在makefile中使用伪目标,常用的伪目标有:all,clean,install,print等。

3.10 make命令的返回值
        make命令执行完成后,可以依据make的返回值查询make运行的结果。可以通过shell命令“echo $?”查看make的返回值。make返回值有以下3种:
1)0:表示成功执行。
2)1:如果make运行时出现任何错误,返回1。
3)2:如果使用了make的-q选项,并且make使得部分目标不需要更新,则返回2。

3.11 makefile知识参考
Ubuntu Wiki:

http://wiki.ubuntu.org.cn/%E8%B7%9F%E6%88%91%E4%B8%80%E8%B5%B7%E5%86%99Makefile:MakeFile%E4%BB%8B%E7%BB%8D
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

4. 创建与使用库  

2011-09-25 23:14:50|  分类: 《Linux编程从入|字号 订阅

4.1  静态库的 创建与使用
        下面通过例子演示如何创建与使用静态库。
例4-1:创建一个C文件add.c,在文件中实现函数Add,并将Add函数添加到静态库libMyFuncs.a中。
add.c文件内容如下:
int Add(int a,int b)
{
    return a+b;
}

编译并生成库文件:
gcc -c add.c
ar r libMyFuncs.a add.o


例4-2:创建另一个C文件sub.c,在文件中实现函数Sub,并将Sub函数添加到静态库libMyFuncs.a中。
sub.c文件内容如下:
int Sub(int a,int b)
{
    return a-b;
}

编译并添加到库文件:
gcc -c sub.c
ar r libMyFuncs.a sub.o

说明:调用ar命令(参数为r)将函数添加到库时,如果函数库文件不存在,则ar将会创建库文件;如果库文件已经存在,则ar只是将函数添加到库文件。

例4-3:打包发布函数库
创建头文件MyFuncs.h,内容如下:
#ifndef MYFUNCS_H
#define MYFUNCS_H
int Add(int a,int b);
int Sub(int a,int b);
#endif

将MyFuncs.h和libMyFuncs.a放到一起,提供给第三方开发使用。

例4-4:创建主程序文件main.c,在主程序中调用libMyFuncs.a中的Add和Sub函数。
main.c内容如下:
#include <stdio.h>
#include "MyFuncs.h"
void main()
{
    int a=1,b=2;
    int res;
    res=Add(a,b);
    printf("a+b=%d\n",res);
    res=Sub(a,b);
    printf("a-b=%d\n",res);
}

编译链接:
gcc main.c -o main -L./ -lMyFuncs
执行:
./main
输出如下:
a+b=3
a-b=-1

4.2 动态库的创建与使用
4.2.1 动态库的创建
将4.1节的add.c和sub.c编译生成动态库libMyFuncs.so,命令如下:
gcc -shared -fPIC -o libMyFuncs.so add.c sub.c

4.2.2 动态库的使用
要调用动态库中的函数,主要有两种方式:
方式1:通过设置环境变量LD_LIBRARY_PATH。
方式2:通过dlopen系列函数。
此处通过方式2调用动态库函数,下面先介绍dlopen系列函数。

4.2.2.1 dlopen函数
dlopen函数打开动态库并映射到当前进程的内存,成功返回句柄,原型如下:
#include <dlfcn.h>
void *dlopen(__const char *__file,int __mode);

参数说明:
1)__file:动态库路径,如果传递的参数只是动态库文件名,那么系统将根据环境变量LD_LIBRARY_PATH的值查找动态库文件。
2)__mode:打开方式,有两种。第一种是RTLD_LAZY,只打开动态库文件,而暂时不解析外部对动态库的引用,直到动态库函数被执行时再解析对动态库的引用;第二种是RTLD_NOW,在dlopen返回前解析所有的对动态库的引用。
返回值:
返回NULL,调用dlopen函数失败,可以调用dlerror获取详细的错误信息;返回其他值,打开成功,返回值为访问动态库的句柄。

4.2.2.2 dlsym函数
dlsym函数的功能是返回一个指向被请求入口点的指针,原型如下:
#include <dlfcn.h>
void *dlsym(void *__handle,__const char *__name);

参数说明:
1)__handle:动态库句柄,该句柄由dlopen函数返回。
2)__name:要调用的函数名称。
返回值:
返回NULL,调用dlsym失败,可以调用dlerror获取详细的错误信息;返回其他值,打开成功,返回值为指向要调用的函数的指针。

4.2.2.3 dlclose函数
dlclose函数的功能是关闭已打开的动态库。原型如下:
#include <dlfcn.h>
#int dlclose(void *__handle);

参数说明:
1)__handle:动态库句柄,该句柄由dlopen函数返回。
返回值:
返回0,调用dlclose成功;返回其他值,调用dlclose失败。

4.2.2.4 dlerror函数
dlerror函数用于输出动态库操作过程中的错误信息。原型如下:
#include <dlfcn.h>
char *dlerror(void);
参数说明:
无参数
返回值:
错误信息指针。

例4-5:使用dlopen系列函数调用动态库libMyFuncs.so中的函数。
创建main.c文件,在文件中输入如下代码:
#include <stdio.h>
#include <dlfcn.h>
#include "MyFuncs.h"
void main()
{
    void *handle; //用于保存动态库句柄
    int (*Add)(int,int); //函数指针
    int (*Sub)(int,int); //函数指针
    //打开动态库
    handle=dlopen("./libMyFuncs.so",RTLD_LAZY);
    if(handle==NULL)
    {
        printf("dlopen:%s\n",dlerror());
        return;
    }
    //清除dl的错误缓冲区,以确保后续调用dlerror函数时不遗留原来的错误信息
    dlerror();
    //导出函数指针
    Add=dlsym(handle,"Add");
    if(Add==NULL)
    {
        printf("dlsym:%s\n",dlerror());
        dlclose(handle);
        return;
    }
    Sub=dlsym(handle,"Sub");
    if(Sub==NULL)
    {
        printf("dlsym:%s\n",dlerror());
        dlclose(handle);
        return;
    }
    //调用动态库里面的函数Add,和Sub
    printf("1+2=%d\n",Add(1,2));
    printf("1-2=%d\n",Sub(1,2));
    //关闭动态库
    dlclose(handle);
}

编译链接:
gcc main.c -o main -ldl
注意:记得加-ldl选项,因为dlopen系列函数是放在动态库libdl.so中的
执行:
./main
运行结果:
1+2=3
1-2=-1
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

5. 文件操作  

2011-10-06 10:05:10|  分类: 《Linux编程从入|字号 订阅

导读:由于本章篇幅较长,所以把它拆分为如下几节。
5.1 文件类型
5.2 文件权限管理之UGO方式
5.3 文件编程的基本概念
5.4 文件基本操作编程
5.5 文件安全编程
5.6 文件属性编程
5.7 目录编程
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、


5.1 文件类型  

2011-10-06 10:06:06|  分类: 《Linux编程从入|字号 订阅

        谈到文件类型,读者肯能立刻联想到Windows环境下的文件类型,如常见的.txt、.doc、.exe等,根据文件的后缀就能判断文件的类型。但是,在Linux系统中,文件的扩展名与文件类型没有必然的联系,文件类型主要与文件的属性有关。虽然如此,读者自行创建的文件,如果有必要还是可以加上扩展名,这样做的目的仅仅是为了在应用时方便。在Linux系统下,查看文件类型可以直接通过执行file命令完成,也可以通过ls -lh来查看某个文件的属性,从而获得文件的类型信息。
        Linux文件类型常见的有普通文件、目录文件、设备文件、管道文件、链接文件等,如下:
1)普通文件:最为常见的文件类型,其特点是不包含有文件系统的结构信息。通常所接触到的文件,如图形文件、数据文件、文档文件、声音文件等都属于这种文件。
2)目录文件:是内核组织文件系统的基本节点。目录文件就是通常所说的文件夹。
3)设备文件:Linux系统为外部设备提供一种标准接口,将外部设备视为一种特殊的文件。通常情况下,Linux系统将设备文件存放在/dev目录下,设备文件使用设备的主设备号和次设备号来指定某外部设备。
4)管道文件:管道文件是一种特殊的文件,主要用于不同进程间的消息传递。
5)链接文件:链接文件是另一种特殊文件,实际上是指向一个真实存在的文件的链接。在Linux系统中,文件链接分为硬链接及符号链接两种。所谓硬链接,就是链接文件与被链接文件物理上是同一个文件,对文件进行硬链接后,只是增加了文件的引用计数,并没有物理上增加文件。硬链接是通过命令ln完成的。符号链接是一个物理上真实存在的文件,但是该文件的内容是指向被链接文件的指针。符号链接文件与原文件的i节点编号是不同的。符号链接通过执行命令ln -s完成。符号链接与Windows系统的快捷方式非常相似。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

5.2 文件权限管理之UGO方式  

2011-10-06 10:08:04|  分类: 《Linux编程从入|字号 订阅

         文件权限,是指对文件或者目录的访问权限,包括对文件的读、写、删除、执行。Linux系统下文件权限管理包括两种不同的类型:一是传统的UGO(User Group Other)方式;一是访问控制列表的ACL(Access Control List)方式。此处只介绍UGO方式。
        UGO方式的权限管理主要是通过文件的9个权限控制位对文件访问进行控制的。这9个权限位可以划分为3组:文件拥有者/用户(File Owner/User)的权限、与文件归属的用户组(Group)同组用户的权限及其他用户(Other)的访问权限。对于每一组权限位,又可以划分为3种不同的权限:读、写及执行,分别用字母r、w、x表示。在传统的文件权限管理模式下,任何文件或者目录都包含这9个权限位。如图5-1所示。
5. 文件操作 - Jim - Jim Liang
图5-1:UGO模式文件权限位

        提示: Linux对文件的权限与目录的权限的定义是不同的。 文件的读权限是指可以读取文件的内容(通过执行命令查看), 目录的读权限是指可以查看(执行ls命令)目录所包含的文件列表; 文件的写权限是指可以对文件的内容进行修改(执行vi命令修改文件内容),而 目录的写权限是指可以在目录中新建或者删除文件的权限; 文件的执行权限是指可以在shell环境下运行该文件,而 目录的执行权限是指可以访问该目录(通过cd命令进入目录)。
        在UGO模式下,可以通过chmod命令修改文件的权限信息,chmod命令提供了两种不同的权限表示方式:
1)字母方式:就是上述的r、w、x。在用字母方式进行权限修改时,用“+”表示增加权限,用“-”表示删除权限。示例:chmod g+r cls(同组用户增加对cls文件的读权限)。
2)数字方式:是将9位权限位按照文件拥有者、组内用户及其他用户分组后,每组内的3位权限位转换为8进制数字表示。转换的具体方式是:如果增加权限,则该位置“1”,否则置“0”,然后每3位为一组,转换为8进制数,形成3个8进制数的权限信息。示例:chmod 751 cls。
        在UGO模式下,新建文件的默认权限与环境变量UMASK有关,这就是当前用户的文件权限掩码,可以通过命令umask进行修改。文件权限掩码的作用过程是:新建文件时,将UMASK值按二进制位取反,与指定的文件权限位进行“按位与”操作,以此来决定文件最终的权限。有关umask的进一步资料,有待以后深入研究时再做笔记。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

5.3 文件编程的基本概念  

2011-10-06 10:09:16|  分类: 《Linux编程从入|字号 订阅

5.3.1 文件描述符
        文件描述符是一个非负整数,其取值范围是0~OPENMAX。当进程创建一个新文件或者打开现有的文件时,内核将向进程返回一个文件描述符。文件描述符在进程范围内是唯一的,并且每个进程可以同时打开的文件数目不能大于OPENMAX。OPENMAX是一个宏定义,它的取值在不同版本的Linux中是不同的。可以通过命令ulimit -n获取它的当前值。当对文件进行I/O操作时,大多数函数都用文件描述符作为参数,代表要操作的文件。

5.3.2 标准输入、标准输出与标准错误输出
        在Linux进程启动时,内核默认为每个进程打开3个文件:标准输入文件、标准输出文件和标准错误输出文件。同样,这3个文件也分配了文件描述符,分别是0、1、2。标准输入文件被映射至键盘,而标准输出文件及标准错误输出文件则被映射至监视器,也就是计算机屏幕。对于这3个已由内核自动打开的文件描述符,在程序中可以直接引用。引用的方式可以是直接用数字,也可以用宏定义:STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。

5.3.3 设置-用户-ID 与 设置-组-ID
        每个文件都有自己的文件属主(用户ID)及组(用户组ID)。如果这个文件是可执行的程序,那么当该文件被执行时就会在操作系统中形成进程。每个进程都有一个有效用户ID和一个有效组ID。这两个与进程相关联的ID的作用是进行文件存取许可检查。除此之外,每个进程还有另外两个ID,分别称作实际用户ID和实际组ID。这两个ID就是执行该程序用户的用户ID及组ID。
        在通常情况下,进程的有效用户ID就是实际用户ID,有效组ID就是实际组ID。但是,Linux提供了一种特殊的机制,可以在文件的属主权限中设置一个标志。这个标志的作用是:当执行该文件时,将进程的有效用户ID设置为该文件所有者的用户ID。如果设置了该标志,那么当一个文件被执行时,其有效用户ID不再是调起该进程的用户ID,而是该文件的属主ID。这种机制就称作设置-用户-ID(suid)。同样,对于组的权限也可以设置这样的标志,称作设置-组-ID(sgid)。suid可以通过命令chmod u+s完成,sgid可以通过命令chmod g+s完成。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

5.4 文件基本操作编程  

2011-10-06 10:11:02|  分类: 《Linux编程从入|字号 订阅

5.4.1 创建文件——creat函数
        Creat函数用来创建一个普通文件,其函数原型为:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *file,mode_t mode);
参数说明:
1)file:要创建文件的路径,可以使用绝对路径,也可以使用相对路径。
2)mode:新建文件的访问权限。文件访问权限的定义在头文件<sys/stat.h>中,如下所示:
S_IRUSR:文件拥有者-读
S_IWUSR:文件拥有者-写
S_IXUSR:文件拥有者-执行
S_IRGRP:组内用户-读
S_IWGRP:组内用户-写
S_IXGRP:组内用户-执行
S_IROTH:其他用户-读
S_IWOTH:其他用户-写
S_IXOTH:其他用户-执行
返回值:
成功,返回值即为文件描述符;失败,返回-1。
注意事项:
1)要在一个目录中新建文件,必须要有对该目录的写权限及执行权限。
2)如果文件已经存在,则调用creat的进程必须对该文件有写权限及执行权限。creat调用成功后,原文件内容被清除,而文件归属的用户ID、组ID及权限信息保留。
3)如果文件不存在,则新生成文件的用户ID及组ID分别是当前进程的有效用户ID及有效组ID。
4)creat调用成功后,文件以只写方式打开。如果在文件操作中需要对文件进行读操作,应先关闭文件后,再用open打开文件进行操作。

例5-1:使用creat在当前目录下创建一个文件拥有者可读写的文件,并在文件中写入文本串。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void main()
{
    //文件描述符
    int fd;
    //用于保存错误信息
    char ErrStr[256];
    //创建文件,使文件属主有读写权限
    fd=creat("HelloWorld.txt",S_IRUSR|S_IWUSR);
    if(fd<0)
    {
        //清空缓冲区
        memset(ErrStr,0,sizeof(ErrStr));
        //向缓冲区中输出错误信息
        sprintf(ErrStr,"creat:%s\n",(char*)strerror(errno));
        //向标准错误设备输出错误信息
        write(STDERR_FILENO,ErrStr,strlen(ErrStr));
        return;
    }
    //向新建立的文件写入Hello world
    write(fd,"Hello world!\n",strlen("Hello world!\n"));
    //记得关闭文件
    close(fd);
}

5.4.2 打开文件——open函数

        open函数用来打开一个现有文件或者创建一个文件,其函数原型为:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *file,int oflag,.../*,mode_t mode*/);
参数说明:
1)file:要打开/创建文件的路径,可以使用绝对路径,也可以使用相对路径。
2)oflag:打开文件的方式。可以由多个方式进行组合,常用的打开文件方式的定义如下:
------------------------------------------------------------------------------------------------------------
打开方式                  意义                                                                              选择
------------------------------------------------------------------------------------------------------------
O_RDONLY             只读打开                                                                       三选一
O_WRONLY            只写打开                                                                       三选一
O_RDWR                 读写打开                                                                      三选一
------------------------------------------------------------------------------------------------------------
O_APPEND             追加方式打开                                                                可选
O_CREAT                如果文件不存在则创建                                                 可选
O_EXCL                  如定义了O_CREAT,且文件已存在,则出错                可选
O_TRUNC               如文件存在,且以只读或只写打开,则截断文件           可选
------------------------------------------------------------------------------------------------------------
3)mode:可选参数,只在创建新文件时有效。用于定义新建文件的访问权限。
返回值:
成功,返回值即为文件描述符;失败,返回-1。
注意事项:
1)文件打开方式参数应在O_RDONLY、O_WRONLY、O_RDWR中选择一个,然后通过按位或“|”与其他的可选参数进行组合。
2)打开方式参数为O_WRONLY|O_CREAT|O_TRUNC的open调用与creat调用是等价的。

例5-2:使用open打开当前目录下的HelloWorld.txt文件,并在文件尾部追加字符串。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void main()
{
    //文件描述符
    int fd;
    //用于保存错误信息
    char ErrStr[256];
    //以追加方式打开文件
    fd=open("HelloWorld.txt",O_WRONLY|O_APPEND);
    if(fd<0)
    {
        //清空缓冲区
        memset(ErrStr,0,sizeof(ErrStr));
        //向缓冲区中输出错误信息
        sprintf(ErrStr,"creat:%s\n",(char*)strerror(errno));
        //向标准错误设备输出错误信息
        write(STDERR_FILENO,ErrStr,strlen(ErrStr));
        return;
    }
    //向文件中追加字符串
    write(fd,"append\n",strlen("append\n"));
    //记得关闭文件
    close(fd);
}

5.4.3 读文件——read函数
        read函数用于从一个打开的文件中读取数据,其函数原型为:
#include <unistd.h>
ssize_t read(int fd,void *buff,size_t count);
参数说明:
1)fd:文件描述符。
2)buff:缓冲区,用于缓存read从文件中读取的数据。
3)count:要读取的字节数。
返回值:
若成功,返回实际读取的字节数;若已读到文件尾,返回0;若出错,返回-1。

例5-3:使用read读取当前目录下的HelloWorld.txt文件,将内容显示在屏幕上。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define READ_LEN 100
void main()
{
    //文件描述符
    int fd;
    //用于保存read函数返回值
    int n;
    //用于保存错误信息
    char ErrStr[256];
    //数据缓冲区
    char DataStr[READ_LEN+1];
    //以只读方式打开文件
    fd=open("HelloWorld.txt",O_RDONLY);
    if(fd<0)
    {
        //清空缓冲区
        memset(ErrStr,0,sizeof(ErrStr));
        //向缓冲区中输出错误信息
        sprintf(ErrStr,"creat:%s\n",(char*)strerror(errno));
        //向标准错误设备输出错误信息
        write(STDERR_FILENO,ErrStr,strlen(ErrStr));
        return;
    }
    //清空缓冲区
    memset(DataStr,0,sizeof(DataStr));
    //从文件中读取READ_LEN个字节
    n=read(fd,DataStr,READ_LEN);
    //读取失败
    if(n<0)
    {
        //清空缓冲区
        memset(ErrStr,0,sizeof(ErrStr));
        //向缓冲区中输出错误信息
        sprintf(ErrStr,"creat:%s\n",(char*)strerror(errno));
        //向标准错误设备输出错误信息
        write(STDERR_FILENO,ErrStr,strlen(ErrStr));
    }
    //已经读到文件末尾
    else if(n!=READ_LEN)
    {
        write(STDOUT_FILENO,DataStr,n);
        write(STDOUT_FILENO,"\nReach tail of file!\n",
              strlen("\nReach tail of file!\n"));
    }
    //正常读取数据
    else
    {
        write(STDOUT_FILENO,DataStr,n);
    }
    //记得关闭文件
    close(fd);
}

5.4.4 写文件——write函数
        write函数用于向一个打开的文件写入数据,其函数原型为:
#include <unistd.h>
ssize_t write(int fd,const void *buff,size_t count);
参数说明:
1)fd:文件描述符。
2)buff:缓冲区,准备写入文件的数据。
3)count:要写入的字节数。
返回值:
若成功,返回实际写入的字节数;若出错,返回-1。出错的常见原因是磁盘已满或者已超过文件最大长度的限制。

5.4.5 关闭文件——close函数
        在文件描述符使用完毕后,应调用close显式关闭文件描述符,否则会导致内存泄漏。close函数的原型为:
#include <unistd.h>
int close(int fd);
参数说明:
1)fd:要关闭的文件描述符。
返回值:
若成功,返回0;若失败,返回-1。

5.4.6 删除文件——unlink函数
        删除文件只是将文件的引用计数减一,只有在文件的引用计数为零的情况下,才会物理删除一个文件。unlink函数的原型为:
#include <fcntl.h>
int unlink(const char *file);
参数说明:
1)file:文件路径。
返回值:
若成功,返回0;若失败,返回-1。

5.4.7 文件的随机存取——lseek函数
        当需要对一个文件进行随机存取时,需要使用lseek函数。其原型为:
#include <unistd.h>
#include <sys/types.h>
off_t lseek(int fd,off_t offset,int whence);
参数说明:
1)fd:文件描述符。
2)offset:文件相对偏移量,偏移的基准是由whence参数决定的。
3)whence:文件偏移依据,取值如下:
SEEK_SET:从文件头开始
SEEK_END:从文件尾开始
SEEK_CRU:从当前位置开始
返回值:
若成功, 返回移动后的文件指针(相对于文件头的偏移量);若失败,返回-1。
注意事项:
1)文件偏移量可以大于文件的当前长度。在这种情况下,对文件的下一次写入将延长该文件,并在文件中构成一个空洞。位于文件中的空洞将会被自动设置为'\0'。
2)可采用这样的方法获取文件长度: FileSize=lseek(fd,0,SEEK_END);

例5-4:在文件HelloWorld.txt中,“Hello World!”的后面插入字符串“Inserted by lseek.”。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
//从文件头部开始算起的插入位置
#define INSERT_POS 13
//插入的数据长度
#define INSERT_LEN 19
void main()
{
    //文件描述符
    int fd;
    //用于保存错误信息
    char ErrStr[256];
    //要移动的数据块长度
    off_t MoveLen;
    //移动数据缓冲区
    char MoveBuff[256];
    //以读写方式打开文件
    fd=open("HelloWorld.txt",O_RDWR);
    if(fd<0)
    {
        //清空缓冲区
        memset(ErrStr,0,sizeof(ErrStr));
        //向缓冲区中输出错误信息
        sprintf(ErrStr,"open:%s\n",(char*)strerror(errno));
        //向标准错误设备输出错误信息
        write(STDERR_FILENO,ErrStr,strlen(ErrStr));
        return;
    }
    //计算要移动的数据块长度
    MoveLen=lseek(fd,0,SEEK_END)-INSERT_POS;
    //移动文件指针到插入位置
    lseek(fd,INSERT_POS,SEEK_SET);
    //清空缓冲区
    memset(MoveBuff,0,sizeof(MoveBuff));
    //将要移动的数据读入缓冲区
    if(read(fd,MoveBuff,MoveLen)<0)
    {
        close(fd);
        //清空缓冲区
        memset(ErrStr,0,sizeof(ErrStr));
        //向缓冲区中输出错误信息
        sprintf(ErrStr,"read:%s\n",(char*)strerror(errno));
        //向标准错误设备输出错误信息
        write(STDERR_FILENO,ErrStr,strlen(ErrStr));
        return;
    }
    //定位文件指针到要移动数据的位置
    lseek(fd,INSERT_LEN+INSERT_POS,SEEK_SET);
    //将移动缓冲区的数据写入新的位置
    write(fd,MoveBuff,MoveLen);
    //再次定位文件指针到插入位置
    lseek(fd,INSERT_POS,SEEK_SET);
    //插入新的数据
    write(fd,"Inserted by lseek.\n",INSERT_LEN);
    //记得关闭文件
    close(fd);
}
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

5.5 文件安全编程  

2011-10-06 10:12:41|  分类: 《Linux编程从入|字号 订阅

         文件在被创建后,可以通过系统调用对文件的属主及权限信息进行修改,Linux为保证文件系统的安全性,修改文件属主及权限信息的操作只允许超级用户root(uid=0)和文件拥有者进行。

5.5.1 文件的属主及用户组编程
        设置文件属主的系统调用有3个,分别是chown、fchown、lchown。chown用于修改文件的属主;fchown与chown的功能是相同的,但是,fchown是对已打开的文件进行修改;lchown用于修改符号链接本身的属主,而不是符号链接所指向的文件。系统调用的原型为:
#include <sys/types.h>
#include <unistd.h>
int chown(const char *file,uid_t owner,gid_t group);
int fchown(int fd,uid_t owner,gid_t group);
int lchown(const char *file,uid_t owner,gid_t group);
参数说明:
1)file:文件路径。
2)fd:文件描述符。
3)owner:目标用户ID。
4)group:目标组ID。
返回值:
若成功,返回0;若失败,返回-1。
注意事项:
1)在只读文件系统上调用chown系列函数将返回失败。
2)对一个符号链接文件用chown修改属主,实际修改的是该符号链接所指向的文件。
3)如果chown系列函数不是由超级用户进程调用,则调用成功后,原来文件设置的suid及sgid位将被清除。

问题:如何查看某用户的uid及gid?
解答:可以使用命令“id username”查看,在笔者的机器里面存在个ljf的用户,执行命令“id ljf ”,输出如下:
uid=1000(ljf) gid=1000(ljf) 组=1000(ljf),4(adm),20(dialout),24(cdrom),46(plugdev),111(lpadmin),119(admin),122(sambashare)
由此可见uid=1000,gid=1000。

例5-5:修改文件HelloWorld.txt的属主为用户ljf。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void main()
{
    if(chown("HelloWorld.txt",1000,1000)<0)
    {
        perror("chown");
    }
    else
        printf("ok: change ower of file!\n");
}

5.5.2 设置文件权限(UGO模式)
        在UGO模式下,对文件权限的修改是通过函数chmod及fchmod完成的。fchmod与chmod实现的功能是相同的。不过fchmod是对文件描述符进行操作。这两个系统调用的原型为:
#include <sys/types.h>
#include <sys/stat.h>
int chmod(const char *file,mode_t mode);
int fchmod(int fd,mode_t mode);
参数说明:
1)file:文件路径。
2)fd:文件描述符。
3)mode:权限组合。
返回值:
若成功,返回0;若失败,返回-1。
注意事项:
1)执行chmod的进程的有效用户ID必须等于文件的拥有者,或者是超级用户,才能修改文件权限信息。
2)调用chmod更改文件权限信息时,修改的是i结点的数据结构,并没有修改文件内容。修改完成后,通过执行ls命令可以看到文件的修改时间并没有变化。
3)实际上,在权限组合参数中,还有一个称作文件粘住位的参数:S_ISVTX。

例5-6:修改文件HelloWorld.txt的文件存取权限为644。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void main()
{
if(chmod("HelloWorld.txt",0644)<0)
{
perror("chmod");
}
else
{
write(STDOUT_FILENO,"Call chmod success.\n",20);
}
}

        前面说到过文件权限屏蔽字umask,一般上,系统中该屏蔽字的默认值是0022(可以通过命令umask查看)。我们也可以通过umask系统调用编程改变这个默认值。umask系统调用的原型为:
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
参数说明:
1)mask:文件权限组合。
返回值:
调用umask前的文件权限屏蔽字。

        前面介绍了设置文件权限的方法,但是,如何获取/测试一个文件的访问权限呢?Linux提供系统调用access满足这个需求。access可以对文件是否具有某种权限进行检测,其原型为:
#include <unistd.h>
int access(const char *file,int mode);
参数说明:
1)file:文件路径。
2)mode:可以是下面取值之一,或者是它们的按位或组合。
------------------------------------------------
mode        说明
------------------------------------------------
R_OK        测试读权限
W_OK        测试写权限
X_OK        测试执行权限
F_OK        测试文件是否存在
------------------------------------------------
返回值:
如果当前进程具有所测试的权限,则返回0;否则,返回-1。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

5.6 文件属性编程  

2011-10-06 10:14:36|  分类: 《Linux编程从入|字号 订阅

5.6.1 文件状态的获取
        在程序设计中,经常需要获取文件的状态信息,如文件的类型、大小及创建时间等。这些信息的获取是通过系统调用stat、fstat和lstat实现的。它们的原型为:
#include <sys/types.h>
#include <sys/stat.h>
int stat(const char *file,struct stat *info);
int fstat(int fd,struct stat *info);
int lstat(const char *file,struct stat *info);
参数说明:
1)file:文件路径。
2)fd:文件描述符。
3)info:指向stat结构体的指针,用于保存获取到的文件状态信息。32位Linux(2.6.35)的stat结构如下:
struct stat
{
    __dev_t st_dev;                   /* Device.  */
    unsigned short int __pad1;
    __ino_t st_ino;                  /* File serial number.*/
    __mode_t st_mode;  /* File mode.  */
    __nlink_t st_nlink;  /* Link count.  */
    __uid_t st_uid;                  /* User ID of the file's owner.*/
    __gid_t st_gid;                  /* Group ID of the file's group.*/
    __dev_t st_rdev;                  /* Device number, if device.  */
    unsigned short int __pad2;
    __off_t st_size;          /* Size of file, in bytes.  */
    __blksize_t st_blksize;                  /* Optimal block size for I/O.  */
    __blkcnt_t st_blocks;          /* Number 512-byte blocks allocated. */
    struct timespec st_atim;  /* Time of last access.  */
    struct timespec st_mtim;  /* Time of last modification.  */
    struct timespec st_ctim;  /* Time of last status change.  */
# define st_atime st_atim.tv_sec  //最后访问(access)时间
# define st_mtime st_mtim.tv_sec     //最后修改(modify)时间
# define st_ctime st_ctim.tv_sec       //最后状态改变(chmod)时间
    unsigned long int __unused4;
    unsigned long int __unused5;
};
返回值:
若成功,返回0;若失败,返回-1。
注意事项:
1)当stat函数对一个符号链接文件操作时,返回的是该符号链接指向的文件的信息。
2)fstat函数与stat函数功能相同,但操作对象不同,它是对文件描述符操作。
3)lstat函数类似于stat,但是当文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是 由该符号链接引用的文件的信息。
4)文件类型的判断:Linux为用户提供了一组宏定义用于对文件的类型进行判断,这组宏定义是根据结构体stat的st_mode成员进行判断的。宏定义如下所示:
---------------------------------------------------------------------------
宏定义                                    意义
---------------------------------------------------------------------------
S_ISREG(st_mode)               普通文件
S_ISDIR(st_mode)                 目录文件
S_ISCHR(st_mode)               字符设备文件
S_ISBLK(st_mode)                块设备文件
S_ISFIFO(st_mode)              管道或FIFO
S_ISLNK(st_mode)                符号链接
S_ISSOCK(st_mode)             套接字
---------------------------------------------------------------------------

例5-7:读取HelloWorld.txt文件的状态,输出文件类型、大小、时间等信息。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <time.h>
void main()
{
//用于保存文件状态的变量
struct stat status={0};
//时间结构体指针
struct tm *ptm;
//中文weekday
const char *weekday[]=
{
"星期日","星期一","星期二","星期三","星期四","星期五","星期六"
};
//读取文件状态
if(stat("HelloWorld.txt",&status)<0)
{
perror("stat");
return;
}
//输出文件类型
if(S_ISREG(status.st_mode))
printf("It is regular file!\n");
else
printf("It is special file!\n");
//输出文件大小
printf("File size=%ld bytes\n",status.st_size);
//输出最后访问时间
ptm=localtime(&status.st_atime);
printf("最后一次访问:%04d年%02d月%02d日 %s %02d时%02d分%02d秒\n",
   ptm->tm_year+1900,ptm->tm_mon+1,ptm->tm_mday,weekday[ptm->tm_wday],
   ptm->tm_hour,ptm->tm_min,ptm->tm_sec);
//输出最后修改时间
ptm=localtime(&status.st_mtime);
printf("最后一次修改:%04d年%02d月%02d日 %s %02d时%02d分%02d秒\n",
   ptm->tm_year+1900,ptm->tm_mon+1,ptm->tm_mday,weekday[ptm->tm_wday],
   ptm->tm_hour,ptm->tm_min,ptm->tm_sec);
//输出最后状态改变时间
ptm=localtime(&status.st_ctime);
printf("最后一次(i节点)状态改变:%04d年%02d月%02d日 %s %02d时%02d分%02d秒\n",
   ptm->tm_year+1900,ptm->tm_mon+1,ptm->tm_mday,weekday[ptm->tm_wday],
   ptm->tm_hour,ptm->tm_min,ptm->tm_sec);
}

5.6.2 更改文件时间信息
        对于系统中的每个文件,Linux在内核中记录了3个时间:文件数据存取时间(st_atime)、文件数据修改时间(st_mtime)、i节点状态改变时间(st_ctime)。其中,st_ctime由文件系统维护,用户程序不能对其进行修改;而对于st_atim和st_mtime,Linux提供了系统调用utime,通过utime应用程序能“恶意”修改文件的这两个时间记录。utime的原型为:
#include <sys/types.h>
#include <utime.h>
int utime(const char *file,const struct utimbuf *times);
参数说明:
1)file:文件路径。
2)times:指向结构体struct utimbuf的指针,如果times为NULL,那么将使用系统的当前时间。struct utimbuf结构体的定义如下:
struct utimbuf
{
    __time_t actime;/* Access time.  */
    __time_t modtime;/* Modification time.  */
};
返回值:
若成功,返回0;若失败,返回-1。

例5-8:更改文件HelloWorld.txt的“访问”时间及“修改”时间为1997年07月01日00时00分00秒。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <utime.h>
void main()
{
//时间结构体
struct tm TM={0};
//时间变量
time_t timesec;
//utime结构体
struct utimbuf times;
//设置时间为1997年07月01日00时00分00秒
TM.tm_year=1997-1900;
TM.tm_mon=7-1;
TM.tm_mday=1;
TM.tm_hour=0;
TM.tm_min=0;
TM.tm_sec=0;
//转换
timesec=mktime(&TM);
//更改时间
times.actime=timesec;
times.modtime=timesec;
if(utime("HelloWorld.txt",&times)<0)
perror("utime");
else
printf("ok: change time record of file!\n");
}
注意观察实验结果,实际情况是utime的执行会导致“访问”时间和i节点“状态改变”时间发生改变,只有“修改”时间能改成“1997年07月01日00时00分00秒”。

5.6.3 文件系统状态的获取
        获取文件系统的状态信息是通过statfs系统调用实现的,其原型为:
#include <sys/types.h>
#include <sys/statfs.h>
int statfs(const char *path,struct statfs *info);
参数说明:
1)path:文件路径。
2)info:指向结构体struct statfs的指针,该结构体的定义如下:
struct statfs
{
    __SWORD_TYPE f_type;                //文件系统类型
    __SWORD_TYPE f_bsize;              //经过优化的传输块大小
    __fsblkcnt_t f_blocks;                      //文件系统数据块总数
    __fsblkcnt_t f_bfree;                       //可用块数
    __fsblkcnt_t f_bavail;                      //非超级用户可获取的块数
    __fsfilcnt_t f_files;                           //文件节点总数
    __fsfilcnt_t f_ffree;                          //可用文件节点数
    __fsid_t f_fsid;                                //文件系统标识
    __SWORD_TYPE f_namelen;        //文件名的最大长度
    __SWORD_TYPE f_frsize;             //分配新的文件存储空间时的最小单位
    __SWORD_TYPE f_spare[5];         //留作扩展
};
返回值:
若成功,返回0;若失败,返回-1。

例5-9:获取根文件系统的总大小、剩余空间及使用率。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/statfs.h>
void main()
{
float total,unused;
//文件系统信息结构体变量
struct statfs fsinfo;
if(statfs("/",&fsinfo)<0)
{
perror("statfs");
return;
}
//输出总大小
total=1.0*fsinfo.f_bsize*fsinfo.f_blocks/1024/1024/1024;
printf("总大小=%0.2f GB\n",total);
//输出剩余空间
unused=1.0*fsinfo.f_bsize*fsinfo.f_bavail/1024/1024/1024;
printf("剩余空间=%0.2f GB\n",unused);
//输出使用率
printf("使用率=%0.2f%%\n",(total-unused)*100/total);
}
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

5.7 目录编程  

2011-10-06 10:16:13|  分类: 《Linux编程从入|字号 订阅

5.7.1 工作目录与用户主目录
        Linux用户在登录系统后,在使用系统的过程中时刻都处于某个目录之中,这个目录就称作工作目录或当前目录。工作目录是可以随时改变的。当前目录用“.”表示,其父目录用“..”表示。
        用户主目录是在创建用户时系统自动创建的,每个用户都有自己的主目录。查看用户主目录可以通过查看文件“/etc/passwd”完成,或者通过环境变量HOME的值获得。在日常应用中,用户可以通过“~”字符来引用自己的主目录。

5.7.2 路径
        路径是指从树型目录中的某个目录层次到另一个目录层次或者文件的一条通路。路径分为绝对路径和相对路径。绝对路径是指从根“/”开始的路径,也成为完全路径;相对路径是指从用户工作目录开始的路径。在树型目录结构中到某个确定目录/文件的绝对路径和相对路径都只有一条。绝对路径是固定不变的,而相对路径则是随着用户工作目录的变化而不断发生变化。

5.7.3 目录的创建
        创建目录的系统调用是mkdir,其原型为:
#incloude <sys/types.h>
#include <sys/stat.h>
int mkdir(const char *path,mode_t mode);
参数说明:
1)path:目录路径。
2)mode:文件权限组合。
返回值:
若成功,返回0;若失败,返回-1。
注意事项:
1)新建目录的实际权限是由mode参数与文件权限掩码umask共同决定的。
2)创建目录成功后,系统将在新的目录项下自动创建两个子目录:“.”和“..”,分别代表当前目录和父目录。

提示:在Linux系统中,还有另一个系统调用可以创建目录,这就是mknod。不过,使用mknod创建目录必须具有超级用户权限。同时,新建的目录中系统不会自动建立“.”和“..”两个目录项。所以,建立目录完成后,是无法执行cd..命令进入上级目录的,必须通过系统调用link建立这两个默认目录项后才能访问。

例5-10:在当前目录创建新目录newdir,并指定权限为0700。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void main()
{
if(mkdir("newdir",0700)<0)
{
perror("mkdir");
return;
}
printf("ok: create dir!\n");
}

5.7.4 目录的删除
        删除目录的系统调用为rmdir,如果目录的引用计数变为0,则释放目录占用的系统空间;否则,只是将目录的引用计数减一。其原型为:
#incloude <sys/types.h>
#include <sys/stat.h>
int rmdir(const char *path);
参数说明:
1)path:目录路径。
返回值:
若成功,返回0;若失败,返回-1。
注意事项:
1)只有在目录中除“.”和“..”外,没有其他文件或目录时,rmdir才能调用成功。前面介绍过的unlink系统调用可以删除非空目录,但是执行删除操作的用户必须具有超级用户权限。

例5-11:删除当前目录下的newdir目录。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void main()
{
if(rmdir("newdir")<0)
{
perror("rmdir");
return;
}
printf("ok: remove dir!\n");
}

5.7.5 改变工作目录
        当前工作目录是进程的属性,而起始目录则是用户的属性。进程调用chdir或fchdir函数可以改变当前工作目录,它们的原型为:
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
参数说明:
1)path:目录路径。
2)fd:目录的文件描述符。
返回值:
若成功,返回0;若失败,返回-1。

5.7.6 获取工作目录
        通过getcwd函数可以获得当前工作目录,其原型为:
#include <unistd.h>
char *getcwd(char *buff,size_t size);
参数说明:
1)buff:保存得到的工作目录名称缓存。
2)size:缓存大小。
返回值:
若成功,返回当前工作目录的名称;若失败,返回NULL。

例5-12:改变工作目录到newdir目录,并获取工作目录的名字。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void main()
{
//工作目录名称缓存
char buff[256];
//工作目录名称指针
char *dirname;
//改变工作目录
if(chdir("newdir")<0)
{
perror("chdir");
return;
}
//获取工作目录
memset(buff,0,sizeof(buff));
dirname=getcwd(buff,256);
if(dirname==NULL)
{
perror("getcwd");
return;
}
//输出工作目录信息
printf("dirname=%s\n",dirname);
printf("buff=%s\n",buff);
}

5.7.7 目录的读取
        Linux提供了几个系统调用专门用于对目录进行读取操作,分别是opendir(打开目录)、readdir(读目录)、rewinddir(复位目录指针)、closedir(关闭目录)。它们的原型为:
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *path);
struct dirent *readdir(DIR *dp);
void rewinddir(DIR *dp);
int closedir(DIR *dp);
参数说明:
1)path:目录路径。
2)dp:DIR结构体的指针。DIR结构体是Linux内核的目录文件结构定义,该结构对于用户来说是透明的,用户只需在程序中直接使用即可。
返回值:
1)opendir:若成功,返回DIR指针;若失败,返回NULL。
2)readdir:系统调用将返回一个指针,其所指向的结构体里保存着目录中下一个目录项的相关数据。后续的readdir调用将返回后续的目录项,直到发生错误或到达目录尾,此时,readdir将返回NULL。其中struct dirent的定义如下:
struct dirent
{
    __ino_t d_ino;                             //目录文件的inode节点编号
    __off_t d_off;                              //目录文件开始至目录进入点的位移
    unsigned short int d_reclen;       //d_name的长度,不包括字符串结束符
    unsigned char d_type;               //d_name所指的文件类型
    char d_name[256];                  //文件或目录名
};
3)rewinddir:系统调用将目录的读指针复位至第一个目录项。无返回值。
4)closedir:若成功,返回0;若失败,返回-1。

例5-13:递归遍历指定的目录,输出所有的目录和文件名字。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <dirent.h>
//子函数:递归遍历指定的目录,输出所有的目录和文件名字
void Traverse(char *dirname)
{
//文件状态信息变量
struct stat status;
//目录结构变量
DIR *pDir;
//目录项结构变量
struct dirent *pDirent;
//打开目录
pDir=opendir(dirname);
if(pDir==NULL)
{
return;
}
//改变工作目录
chdir(dirname);
//循环读取目录pDir中的目录项
while(pDirent=readdir(pDir))
{
memset(&status,0,sizeof(status));
stat(pDirent->d_name,&status);
//如果为目录
if(S_ISDIR(status.st_mode))
{
//确保不是.和..目录
if(memcmp(pDirent->d_name,".",1)!=0&&memcmp(pDirent->d_name,"..",2)!=0)
{
printf("%s/%s\n",dirname,pDirent->d_name);
//递归搜索
Traverse(pDirent->d_name);
}
}
//否则
else
{
printf("%s/%s\n",dirname,pDirent->d_name);
}
}
//关闭目录
if(closedir(pDir)<0)
   perror("closedir");
}
//主函数
void main(int argc,char *argv[])
{
if(argc!=2)
{
printf("usage:traverse path\n");
return;
}
Traverse(argv[1]);
}
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

6. 标准输入/输出库  

2011-10-07 23:26:29|  分类: 《Linux编程从入|字号 订阅

        在上一章中介绍了Linux系统下的文件操作,这些文件操作都是直接通过文件操作的系统调用(也称文件操作API)完成的。在Linux系统中,还提供了另外一种访问文件的方式:标准输入/输出库(以下简称标准I/O)。标准I/O库是在文件操作的系统调用基础上进行封装的,更加便于应用开发人员使用。
        使用文件操作API访问文件系统,是针对文件描述符进行的;而使用标准I/O库时,所操作的则是一个名为“流”的对象。对于每一个进程来说,系统默认将为其打开3个流:标准输入、标准输出和标准错误输出。在系统中分别定义了全局指针(参考stdio.h)stdin、stdout和stderr,它们内部分别指向文件描述符 STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO。在默认情况下,程序从键盘读取stdin,将向stdout和stderr输出的信息显示到屏幕上。如果要改变这种默认的输入输出方式,需要使用一种名称为“输入/输出重定向”的技术。

6.1 缓存
        标准I/O库与文件操作API的基本区别在于:标准I/O库是带缓存的,而文件操作API是无缓存机制的。缓存是在第一次调用标准I/O库进行I/O操作时,由系统自动调用malloc分配的。标准I/O库提供的缓存方式包括以下3种:
1)全缓存(#define _IOFBF 0):是指只有当前I/O操作的缓存被填满时,才会向文件系统进行刷新。大多数的标准I/O函数都是基于这种缓存方式的。
2)行缓存(#define _IOLBF 1):是指在输入/输出过程中,如果遇到换行符,则向文件系统进行刷新。如在向标准输出stdout输出信息时,默认也是以行缓存的方式进行的。
3)不缓存(#define _IONBF 2):是指不设缓存机制的标准I/O。在某些情况下是不适宜设置缓存的,如标准错误输出就是不设缓存的I/O操作。

提示:除由系统自动进行缓存刷新操作外,也可以随时调用fflush系统调用手工刷新缓存。

6.2 设置缓存方式
        标准I/O库提供了改变缓存方式的几个函数,它们的原型为:
#include <stdio.h>
void setbuf(FILE *fp,char *buff);
void setbuffer(FILE *fp,char *buff,size_t size);
void setlinebuf(FILE *fp);
int setvbuf(FILE *fp,char *buff,int mode,size_t size);
参数说明:
1)fp:指向FILE结构(流对象)的指针。
2)buff:自定义缓冲区指针。如果该参数为空(NULL),表示设置流对象为无缓存的模式(_IONBF)。对于setbuf来说,该缓冲区的大小固定为BUFSIZ。而对于setbuffer,缓冲区的大小由参数size指定。
3)size:自定义缓冲区的大小,以字节为单位。
4)mode:缓存模式,可以为_IOFBF,_IOLBF或_IONBF之一。
返回值:
1)setvbuf:若成功,返回0;若失败,返回其他值。
注意事项:
1)要设置一个流为无缓存模式,只需调用setbuf(fp,NULL)即可。
2)可以用setvbuf函数实现其他几个函数的功能。对于setbuf可以用setvbuf代替实现为:setvbuf(fp,buff,buff?_IOFBF:_IONBF,BUFSIZ);而setlinebuf的功能可以用如下语句替代实现:setvbuf(fp,(char*)NULL,_IOLBF,0)。

例6-1:测试stdout的默认缓存模式输出,fflush刷新效果以及关闭缓存模式后的输出。
代码如下:
#include <stdio.h>
void main()
{
//测试默认缓存模式
fprintf(stdout,"这时标准输出1 ");
fprintf(stderr,"这时标准错误输出1 ");
fprintf(stdout,"\n");
//测试fflush
fprintf(stdout,"这时标准输出2 ");
fflush(stdout);
fprintf(stderr,"这时标准错误输出2 ");
fprintf(stdout,"\n");
//关闭标准输出的缓存模式
setbuf(stdout,NULL);
fprintf(stdout,"这时标准输出3 ");
fprintf(stderr,"这时标准错误输出3 ");
fprintf(stdout,"\n");
}
编译运行,结果如下:
这是标准错误输出1 这是标准输出1 
这是标准输出2 这是标准错误输出2 
这是标准输出3 这是标准错误输出3

6.3 输入输出重定向
        所谓输入/输出重定向,就是将默认的输入/输出的目标重新定向到新的目标,新目标可能是某个文件,也可能是某个设备。比较典型的重定向例子是错误输出重定向,为了跟踪错误及对错误进行留迹处理,有时需要把错误信息保存到文件中。在Linux的shell环境下,输入/输出重定向请参考本栏目第1章《1. shell编程》。

例6-2:编程实现标准输出重定向,将标准输出重定向到工作目录的stdout.txt文件中。
代码如下:
#include <stdio.h>
#include <unistd.h>
void main()
{
//流指针
FILE *fp;
fp=fopen("stdout.txt","w");
if(fp==NULL)
{
perror("fopen");
return;
}
//关闭标准输出设备,否则将会导致内存泄漏
close(STDOUT_FILENO);
//复制stdout.txt的文件描述符至stdout的文件描述符
if(dup2(fileno(fp),fileno(stdout))<0)
{
perror("dup2");
fclose(fp);
return;
}
//向标准输出设备输出测试信息,经过重定向,应该会输出到stdout.txt文件中
printf("This is test message by printf.\n");
fprintf(stdout,"This is test message by fprintf.\n");
//记得关闭流
fclose(fp);
}
代码解析:
1)在本例中,首先打开要重定向的目标文件,然后关闭标准输出,并调用dup2复制目标文件的描述符至标准输出。经过如此处理后,在向标准输出设备输出信息时,将重定向至目标文件。
2)本例中的fileno系统调用的作用是取得某个流的文件描述符。其中,fileno(stdout)与直接使用1是等价的。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

7. 进程  

2011-10-09 14:08:59|  分类: 《Linux编程从入|字号 订阅

导读:由于本章篇幅较长,所以把它拆分为如下几节。
7.1 进程的基本概念
7.2 进程的运行环境
7.3 进程的创建
7.4 进程的终止
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

7.1 进程的基本概念  

2011-10-09 17:33:58|  分类: 《Linux编程从入|字号 订阅

        简单地说,进程是指处于运行状态的程序。程序是静态的保存在磁盘上的代码和数据的组合,而进程是动态的概念。

7.1.1 进程的属性
        进程创建后,系统内核为其分配了一系列的数据结构。这些数据结构中保存了进程的相关属性。主要的进程属性包括以下几种。
1)进程的标识符(ID):进程创建时,内核为其分配一个惟一的标识符,该标识符是一个short类型的非负整数。进程ID是由系统循环使用的,如果当前可用进程号超过了最大值,将从0开始选择可用的整数继续循环使用。
2)父进程标识符:Linux下的全部进程组成一棵进程树,其中树根进程是0号进程swapper。除根进程外,每个进程都有其对应的父进程。
3)用户标识符:是指运行该程序的用户ID。
4)组标识符:是指运行该程序的用户所归属的组ID。
5)有效用户标识符:是指该进程运行过程中有效的用户身份。在进行文件权限许可检查时,以该有效用户标识为依据。
6)有效组标识符:是指该进程运行过程中有效的组标识。在进行文件权限许可检查时,以该有效组标识为依据。
7)进程组标识符:一个进程可以属于某个进程组。通过设置进程组,可以实现向一组进程发送信号等进程控制操作。
8)会话标识符:每个进程都属于惟一的会话。

        Linux提供了一组系统调用用于获取进程的上述属性,它们的原型为:
#include <unistd.h>
__pid_t getpid();                          //获取当前进程的进程ID
__pid_t getppid();                        //获取当前进程的父进程ID
__pid_t getpgrp();                       //获取当前进程的进程组ID
__uid_t getuid();                         //获取当前进程的实际用户ID
__uid_t geteuid();                       //获取当前进程的有效用户ID
__gid_t getgid();                         //获取当前进程的实际用户组ID
__gid_t getegid();                       //获取当前进程的有效用户组ID
__pid_t getsid(__pid_t pid);        //获取指定进程的会话ID
参数说明:
1)pid:进程的标识符。
返回值:
若失败,返回-1;若成功,返回获取到的进程属性信息。

7.1.2 进程的内存映像
        在系统内存映像中,进程主要包括代码段、数据段、BSS段、堆栈段,各元素的含义如下。
1)代码段:用来存放可执行文件的指令,是可执行程序在内存中的映像。对代码段的访问有严格安全检查机制,以防止在运行时被非法修改,代码段是只读的。
2)数据段:用来存放程序中已初始化的全局变量。
3)BSS段:用来存放程序中未初始化的全局变量。
4)堆(heap):用于存放进程在运行过程中动态分配的内存。
5)栈(stack):用于存放局部变量以及函数调用的现场。

7.1.3 进程组
        在Linux系统中,每个进程都惟一地归属于某个进程组。在shell环境中,一条Linux命令就形成一个进程组。这条命令可以只包含一个命令,也可以是通过管道符连接起来的若干命令。每个进程组都有一个组长进程。进程组的ID就是这个 组长的ID。当进程组内的所有进程都结束或者加入到其他进程组内时,该进程组就结束了。
        可以通过系统调用setpgid和segpgrp修改某个进程的进程组,其原型为:
#include <unistd.h>
int setpgid(__pid_t pid,__pid_t pgid);
int setpgrp (void);
参数说明:
1)pid:用于指定要修改的进程ID,如果该参数为0,则指当前进程ID。
2)pgid:用于指定新的进程组ID,如果该参数为0,则指当前进程ID。
返回值:
若成功,返回0;若失败,返回-1。
注意事项:
1)setpgid函数用于修改某个进程(不一定是自身)的进程组。
2)setpgrp函数用于设置当前进程为进程组的组长。调用该函数后,将产生一个新的进程组,进程组的组长ID即为当前进程的ID。
3)setpgrp函数的功能等价于setpgid(0,0)。

7.1.4 进程的会话
        当用户登录一个新的shell环境时,一个新的会话就产生了。一个会话可以包括若干个进程组,但是这些进程组中只能有一个前台进程组,其他的为后台进程组。前台进程组通过其组长进程与控制终端相连接,接收来自控制终端的输入及信号。一个会话由会话ID来标识,会话ID是会话首进程的进程ID。
        Linux提供了系统调用setsid用于产生一个新的会话。不过,调用setsid的进程应该保证不是某个进程组的组长进程。setsid调用成功后,将生成一个新的会话。新会话的会话ID是调用进程的进程ID。新会话中只包含一个进程组,该进程组只包含一个进程(调用setsid的进程),且该会话没有控制终端,也就是说该进程将会变成后台进程。setsid的原型为:
#include <unistd.h>
__pid_t setsid();
返回值:
若失败,返回-1,典型的错误是调用进程是某个进程组的组长,此时错误码errno为EPERM;若成功,返回进程的进程组ID。

7.1.5 进程的控制终端
        在Linux系统中,每个终端设备都有一个设备文件与其相关联,这些终端设备成为tty。可以通过tty命令查看当前终端的名称。用户可以通过telnet远程登录到某个Linux系统,此时其实并没有真正的终端设备。这种情况下,Linux系统将为用户自动分配一个成为“伪终端”的终端设备,伪终端的设备文件名称类似/dev/pts/???。
        在Linux进程环境中,有一个成为“控制终端”的概念。所谓控制终端,就是指一个进程运行时,进程与用户进行交互的界面。一个进程从终端启动后,这个进程的运行过程就与控制终端密切相关。可以通过控制终端输入/输出,也可以通过控制终端向进程发送信号。当控制终端被关闭时,该控制终端所关联的进程将收到SIGHUP信号,系统对该信号的缺省处理方式就是终止进程。

7.1.6 进程的状态
        进程是由操作系统内核调度运行的,在调度过程中,进程的状态是不断发生变化的。这些状态包括以下几种。
1)可运行状态(RUNNING):该状态有两种情况,一是进程正在运行;二是处于就绪状态,只要得到CPU就可以立即投入运行。
2)等待状态(SLEEPING):表明进程正在等待某个事件发生或者等待某种资源。该状态可以分成两类:可中断的和不可中断的。处于可中断等待状态的进程,既可以被信号中断,也可以由于资源就绪而被唤醒进入运行状态。而不可中断等待状态的进程在任何情况下都不可中断,只有在等待的资源准备好后方可被唤醒。
3)暂停状态(STOPPED):进程接收到某个信号,暂时停止运行。大多数进程是由于处于调试中,才会出现该状态。
4)僵尸状态(ZOMBIE):表示进程结束但尚未消亡的一种状态。一个进程结束运行退出时,就处于僵尸状态。进程会在退出前向其父进程发送SIGCLD信号,父进程应该调用wait为子进程的退出做最后的收尾工作。如果父进程未进行该工作,则子进程虽然已退出,但通过执行ps命令仍然可以看到该进程,这样的进程成为僵尸进程。

7.1.7 进程的优先级
        进程的优先级定义了进程被调度的优先顺序,优先级的数值越小,则进程的优先权越高。进程的优先级是由进程的优先级别(PR)和进程的谦让值(NI)两个因素共同确定的。内核在调度进程时,将优先级别(PR)和谦让值(NI)相加以确定进程的真正优先级。对于一个进程来说,其优先级别(PR)是由父进程继承而来的,用户进程不可更改,但是可以更改谦让值(NI)。进程的谦让值在进程被创建时置为默认值0,系统允许的谦让值范围为最高优先级的-20到最低优先级的19。
        为方便用户操作进程的优先级,Linux提供了几个系统调用以修改/获取进程的谦让值。这些系统调用的原型为:
#include <unistd.h>
#include <sys/resource.h>
int nice(int ni);                                                                      //设置当前进程的谦让值
int setpriority(__priority_which_t which,id_t who,int ni);       //设置指定进程的谦让值
int getpriority(__priority_which_t which,id_t who);               //获取指定进程的谦让值
参数说明:
1)ni:要设置的谦让值,取值范围为-20~19。
2)which:指定设置/获取谦让值的目标类型。共有3种目标类型,分别是PRIO_PROCESS(设置/获取某进程的谦让值)、PRIO_PGRP(设置/获取某进程组的谦让值)和PRIO_USER(设置/获取某个用户的所有进程的谦让值)。
3)who:指定设置/获取谦让值的目标。对于PRIO_PROCESS类型的目标,该参数为进程的ID;对于PRIO_PGRP类型的目标,该参数为进程组ID;对于PRIO_USER类型的目标,该参数为用户ID。如果该参数为0,对于3种目标类型,分别表示当前进程、当前进程组、当前用户。
返回值:
1)nice:若成功,返回0;若失败,返回-1。
2)setpriority:若成功,返回0;若失败,返回-1。
3)getpriority:该系统调用比较特殊,可能返回负值,所以无法直接根据返回值确定是否调用成功。推荐的方法是,在调用该函数前置errno为0,如果调用后errno仍为0,表明成功,否则表明调用失败。

例7-1:修改当前进程的谦让值,完成后输出进程的谦让值。
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/resource.h>
void main()
{
//用于保存谦让值
int ni;
//置errno为0
errno=0;
ni=getpriority(PRIO_PROCESS,getpid());
//错误处理
if(errno!=0)
{
perror("getpriority");
return;
}
//显示谦让值
printf("priority NI is %d\n",ni);
//设置进程的谦让值为3
if(nice(3)<0)
{
perror("nice");
return;
}
printf("ok: set priority NI to 3\n");
//置errno为0
errno=0;
ni=getpriority(PRIO_PROCESS,getpid());
//错误处理
if(errno!=0)
{
perror("getpriority");
return;
}
//显示谦让值
printf("priority is %d\n",ni);
}

提示:除了nice系统调用外,也可以通过nice和rnice命令来修改一个进程的谦让值。nice可以在执行一个程序时,直接指定谦让值;而rnice命令则可以修改一个正在运行的进程的谦让值。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

7.2 进程的运行环境  

2011-10-16 17:34:00|  分类: 《Linux编程从入|字号 订阅

7.2.1 进程的入口函数
       众所周知,C语言的入口函数是main,进程开始执行时,都是从main函数开始的。所以,在一个可执行的Linux程序中,必须包含有main函数。main函数的完整原型为:
int main(int argc,char *argv[],char *env[]);
参数说明:
1)argc:程序执行时的命令行参数个数,该参数个数包含了程序名称本身。
2)argv:命令行参数数组,其中每个数组成员为一个命令行参数,程序名称对应该数组第一个成员argv[0]。各命令行参数是以空格分隔的。
3)env:环境变量数组,可以在程序中访问这些变量。
返回值:
main函数的返回值可以在shell中通过命令echo $?获取。
注意事项:
1)main函数可以有多种格式,上面给出的是完成的格式。根据具体应用main函数还可以有如下格式:
void main(void);
main(void);
int main(void);
int main(int argc,char *argv[]);
等等。

例7-2:演示完整的main函数。
代码如下:
#include <stdio.h>
int main(int argc,char *argv[],char *env[])
{
int i;
//输出命令行参数
for(i=0;i<argc;i++)
printf("argv[%d]=%s\n",i,argv[i]);
//输出环境变量
for(i=0;env[i]!=NULL;i++)
printf("env[%d]=%s\n",i,env[i]);
//返回命令行参数个数
return argc;
}

7.2.2 getopt
        从上面的介绍可以知道,在编程时可以通过直接访问命令行参数数组的方法获取命令行参数。另外,Linux还提供了专门的系统调用getopt获取命令行参数。getopt提供更为强大的获取命令行参数的方法:可以按照规则解析命令行参数。如执行下述程序:./test_program -i -s abc -t,在这种情况下,要获取到选项-s的参数abc需要进行很复杂的判断;而通过getopt可以很简单地解决这个问题。getopt系统调用的原型为:
#include <getopt.h>
int getopt(int argc,char *const *argv,const char *shortopts);
参数说明:
1)argc:通常由main函数的argc传递。
2)argv:通常由main函数的argv传递。
3)shortopts:选项字符串,该参数指定了解析命令行参数的规则。getopt认可的命令行选项参数是通过“-”进行的,例如,ls -l中的“-l”。该参数中,如果某个选项有输入数据,则在该选项字符的后面应该包含“:”,如ls -l ./a,在指定本参数时,应该用“l:”。
返回值:
1)-1:解析失败或者解析完毕。
2)其他:返回解析成功的选项字符。
注意事项:
1)为了澄清概念,在本文中区分选项和参数的概念,如命令“cmd -a 123”中,“-a”为选项,“123”是选项“-a”的参数。
2)getopt在解析过程中会涉及到4个全局变量,这4个全局变量都在头文件getopt.h中声明,分别是:
opterr:控制getopt错误信息的输出,设置为0时getopt遇到错误时不输出信息,否则输出错误信息;
optarg: 指向当前选项参数(如果有)的指针;
optind:再次调用 getopt() 时的下一个 argv 指针的索引;
optopt:最后一个已知选项。
3)可以利用optind、optopt和argc变量判断getopt函数解析命令行选项是否成功,例如,当optind不等于argc时说明getopt碰到不认识的选项;当optind等于argc时,但如果此时optopt不等于0,则碰到的最后一个选项需要用户输入参数,但是用户没有输入参数;当optind等于argc时,如果此时optopt等于0,那么getopt函数成功地解析了用户输入的所有选项。
4)getopt只能支持单字符的选项,如“-l”、“-a”等。如果需要支持多字符的选项,如“-file”等,就需要用到getopt_long函数。

例7-3:设计一个程序,读取用户的命令行输入,根据输入选项执行响应的输出,如果输入错误则提示错误信息。
代码如下(getopttest.c):
#include <stdio.h>
#include <getopt.h>
void main(int argc,char *argv[])
{
int ch;
//如果没有输入任何选项
if(argc<2)
{
printf("usage: appname -a -b -c -d param -e param -f param\n");
return;
}
//禁止getopt函数输出错误信息
opterr=0;
//循环调用getopt函数解析命令行
while((ch=getopt(argc,argv,"abcd:e:f:"))!=-1)
{
//根据不同的选项作不同的处理
switch(ch)
{
case 'a':
printf("ok: option -a\n");
break;
case 'b':
printf("ok: option -b\n");
break;
case 'c':
printf("ok: option -c\n");
break;
case 'd':
printf("ok: option -d: %s\n",optarg);
break;
case 'e':
printf("ok: option -e: %s\n",optarg);
break;
case 'f':
printf("ok: option -f: %s\n",optarg);
break;
defualt:
printf("error: other option -%c\n",ch);
}
}
//如果碰到不认识的选项
if(optind!=argc)
printf("error: unknown option: %s\n",argv[optind]);
//如果最后一个选项需要输入参数但用户没有输入
else if(optopt!=0)
printf("error: option -%c needs parameter\n",optopt);
//成功地解析了用户输入的所有选项
else
printf("ok: parse options successful!\n");
}
说明:程序要求用户的输入命令行语法为:appname -a -b -c -d param -e param -f param。
编译: gcc getopttest.c -o getopttest
运行: ./getopttest -a -b -c -d paramd -e parame -f paramf
输出:
ok: option -a
ok: option -b
ok: option -c
ok: option -d: paramd
ok: option -e: parame
ok: option -f: paramf
ok: parse options successful!
运行: ./getopttest -b -e 123
输出:
ok: option -b
ok: option -e: 123
ok: parse options successful!
运行: ./getopttest -b 123
输出:
ok: option -b
error: unknown option: 123
运行: ./getopttest -a -d hello -f
输出:
ok: option -a
ok: option -d: hello
error: option -f needs parameter

7.2.3 getopt_long
        当碰到大于1个字符长度的选项(如“-file”)时getopt无能为力,此时就需要使用另一个函数getopt_long,该函数原型为:
#include <getopt.h>
int getopt_long(int argc, char * const argv[],const char *sortopts,const struct option *longopts, int *longindex);
参数说明:
1)argc:同getopt函数的argc。
2)argv:同getopt函数的argv。
3)shortopts:同getopt函数的shortopts。
4)longopts:struct option类型的数组。struct option结构如下所示:
struct option
{
const char *name;
int has_arg;
int *flag;
int val;
};
其中,
name:多字符的选项名称。
has_arg:选项是否有参数,取值0(no_argument)表示选项没有参数;取值1(required_argument)表示选项有参数;取值2(optional_argument)表示选项参数可选。
flag:如果该成员定义为NULL,那么getopt_long的返回值为该结构val字段值;如果该成员不为NULL而是指向一个变量,那么getopt_long调用后将在所指向的变量中填入val值,并且getopt_long返回0。通常该成员定义为NULL即可。
val:该长选项对应的短选项名称。
5)longinddex:输出参数,如果该参数不为NULL,那么它是一个指向整型变量的指针,在getopt_long运行时,该整型变量会被赋值为获取到的选项在结构数组longopts中的索引值。
返回值:
1)-1:解析失败或者解析完毕。
2)0:如果longopts的成员flag不为NULL,那么如果getopt_long解析成功,则返回0。
2)其他:返回解析成功的选项字符(val值)。
注意事项:
1)getopt_long在解析过程中也会涉及到4个全局变量otperr、optarg、optind和optopt,可以充分利用这几个变量以协助程序设计。

例7-4:编程实现长选项名字的解析。
代码如下:
#include <stdio.h>
#include <getopt.h>
struct option longopts[]=
{
{"name",required_argument,NULL,'n'},
{"passwd",required_argument,NULL,'p'},
{"flag",no_argument,NULL,'f'},
{NULL,0,NULL,0}
};
void main(int argc,char *argv[])
{
int ch;
//如果没有输入任何选项
if(argc<2)
{
printf("usage: appname --name username --passwd password --flag\n");
return;
}
//禁止getopt_long函数输出错误信息
opterr=0;
//循环解析命令行
while((ch=getopt_long(argc,argv,"n:p:o:",longopts,NULL))!=-1)
{
printf("optind=%d\n",optind);
switch(ch)
{
case 'n':
printf("ok: option -n/--name: %s\n",optarg);
break;
case 'p':
printf("ok: option -p/--passwd: %s\n",optarg);
break;
case 'o':
printf("ok: option -f/--flag\n");
break;
defualt:
printf("error: other option -%c\n",ch);
}
}
//如果碰到不认识的选项
if(optind!=argc)
printf("error: unknown option: %s\n",argv[optind]);
//如果最后一个选项需要输入参数但用户没有输入
else if(optopt!=0)
printf("error: option -%c needs parameter\n",optopt);
//成功地解析了用户输入的所有选项
else
printf("ok: parse options successful!\n");
}

7.2.4 进程的环境变量
        在编程过程中,有时可以利用环境变量简化编程工作。Linux提供了两个系统调用getenv和putenv用于获取和设置环境变量,它们的原型为:
#include <stdlib.h>
char *getenv(const char *name);
int putenv(char *namevalue);
参数说明:
1)name:环境变量名称。
2)namevalue:要设置的环境变量串,格式为“环境变量名=值”。
返回值:
1)getenv:返回NULL,表示相关的环境变量未定义;返回其它,环境变量的值。
2)putenv:返回0,成功;返回-1,失败。

例7-5:读取环境变量CONFIG_PATH的值,如果该环境变量不存在则创建。
代码如下:
#include <stdio.h>
#include <stdlib.h>
void main()
{
char *str;
//获取环境变量
str=getenv("CONFIG_PATH");
//如果环境变量没有定义
if(str==NULL)
{
printf("warning: environment variable CONFIG_PATH is not defined!\n");
//创建环境变量
if(putenv("CONFIG_PATH=/etc")<0)
{
perror("putenv");
return;
}
else
{
printf("ok: create environment variable CONFIG_PATH!\n");
//重新获取环境变量
str=getenv("CONFIG_PATH");
if(str==NULL)
{
perror("getenv");
return;
}
}
}
//打印环境变量
printf("CONFIG_PATH=%s\n",str);
}

提示:Linux提供了另外两个系统调用setenv和unsetenv用于修改环境变量,它们实现的功能与putenv大同小异。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

7.3 进程的创建  

2011-10-16 21:14:03|  分类: 《Linux编程从入|字号 订阅

        Linux提供了多种创建进程的方法,这些方法主要包括fork系统调用、exec函数族和system系统调用。

7.3.1 调用fork创建进程
        fork调用成功后,将并发出新的进程,此时,新生成的进程称为子进程,而原来的调用进程称为父进程。fork系统调用是非常特殊的一个系统调用,调用fork一次将返回两次,分别在父进程和子进程中返回。在父进程中,其返回值为子进程的进程标识符;在子进程中,其返回值为0。
        fork调用成功后,产生的子进程继承了父进程大部分的属性,这些属性主要包括以下几点:
1)进程的实际用户ID、实际用户组ID和有效用户ID、有效用户组ID。
2)进程组ID、会话ID及控制终端。
3)当前工作目录及根目录。
4)文件创建掩码UMASK。
5)环境变量。
6)父进程中已经打开的描述符(如文件描述符、套接口描述符等)。
        除此之外,也有一部分进程属性是不能直接从父进程那里继承的,主要包括以下几点:
1)进程ID。
2)用户时间和系统时间,这两个时间被初始化为0。
3)超时时钟设置为0,这个时钟是由alarm系统调用使用的。
4)信号处理函数指针组置为空。
5)父进程的记录锁。
        fork系统调用的原型为:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回值:
返回值为pid_t类型,pid_t是一个宏定义,其实质是int,被定义在头文件<sys/types.h>中。fork调用成功后在父进程中返回子进程ID,在子进程中返回0;若出错,返回-1。
注意事项:
1)由于子进程继承了父进程中已打开的文件描述符,在这种情况下,这些描述符的引用计数已经加一(每fork一次就加一)。因此,在关闭这些描述符时,要记住多次关闭直至描述符的引用计数为0。
2)子进程复制了父进程的数据段,有其独立的地址空间。父子进程各有一份全局变量的拷贝。因此,不能通过全局变量在父子进程间进行通信,而要通过专门的进程间通信机制。

例7-6:编程创建多个进程,每个进程输出自己的相关信息。
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void main()
{
pid_t pid;
//忽略SIGCLD信号,避免形成僵尸进程
signal(SIGCLD,SIG_IGN);
//创建进程
pid=fork();
switch(pid)
{
//创建子进程失败
case -1:
perror("fork");
break;
//子进程
case 0:
printf("This is child process, my PID=%d\n",getpid());
break;
//父进程
default:
printf("This is parent process, my PID=%d, my child PID=%d\n",getpid(),pid);
sleep(1);
}
}

提示:Linux提供了另外一个系统调用vfork,它也是用来创建子进程,与fork不同的是,vfork创建子进程的目的是调用exec,并且vfork产生的子进程与父进程共享大多数进程空间。也就是说,vfork调用成功后,父、子进程共享数据段。关于vfork的使用有待以后需要使用时再作深入学习。

7.3.2 调用exec系列函数创建进程
        exec系列函数并不创建新进程,调用exec前后的进程ID是相同的。exec函数的主要工作是清除父进程的可执行代码映像,用新程序的代码覆盖调用exec的进程代码。如果exec执行成功,进程将从新程序的main函数入口开始执行。调用exec后,除进程ID保持不变外,还有下列进程属性也保持不变。
1)进程的父进程ID。
2)实际用户ID和实际用户组ID。
3)进程组ID、会话ID和控制终端。
4)定时器的剩余时间。
5)当前工作目录及根目录。
6)文件创建掩码UMASK。
7)进程的信号掩码。
        exec系列函数共有6种不同的形式,统称为exec函数。为讲解清晰,把这6个函数划分为两组:一组是execl、execle和execlp;另一组是execv、execve和execvp。这两组函数的不同在于exec后的第一个字符,第一组是l,在此称为execl系列;第二组是v,在此称为execv系列。这里的l是list(列表)的意思,表示execl系列函数需要将每个命令行参数作为函数的参数进行传递。而v是vector(矢量)的意思,表示execv系列函数将所有函数包装到一个矢量数组中传递即可。exec函数的原型为:
#include <unistd.h>
int execv(const char *path,char* const argv[]);
int execve(const char *path, char* const argv[],char* const envp[]);
int execvp(const char *file,char* const argv[]);
int execl(const char *path,const char *arg,...);
int execle(const char *path,const char *arg,...);
int execlp(const char *file,const char *arg,...);
参数说明:
1)path:要执行的程序路径。可以是绝对路径或者是相对路径。在execv、execve、execl和execle这四个函数中,使用带路径名的文件名作为参数。
2)file:要执行的程序名称。如果该参数中包含“/”字符,则视为路径名直接执行;否则视为单独的文件名,系统将根据PATH环境变量指定的路径顺序搜索指定的文件。
3)argv:命令行参数的矢量数组。
4)envp:带有该参数的exec函数,可以在调用时指定一个环境变量数组。其他不带该参数的exec函数,则使用调用进程的环境变量。
5)arg:程序的第0个参数,即程序名自身,相当于argv[0]。
6)...:命令行参数列表。调用相应程序时有多少命令行参数,就需要有多少个输入参数项。注意:在使用此类函数时,在所有命令行参数的最后,应该增加一个空的参数项(NULL),表明命令行参数结束。
返回值:
1)-1:表明调用exec失败。
2)无返回:表明调用成功。由于调用成功后,当前进程的代码空间被新进程覆盖,所以无返回。

例7-7:编程实现调用执行ls命令输出当前目录的文件列表。
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
void main()
{
pid_t pid;
//定义参数数组,为execv所使用
char *param[]={"ls","-a",NULL};
//创建子进程
pid=fork();
if(pid<0)
{
perror("fork");
return;
}
//在子进程中执行“ls -l”命令
if(pid==0)
{
//子进程调用execl后进程空间将被ls命令覆盖
if(execl("/bin/ls","ls","-l",NULL)<0)
perror("execl");
}
sleep(1);
//父进程继续创建子进程
pid=fork();
if(pid<0)
{
perror("fork");
return;
}
//在子进程中执行“ls -a”命令
if(pid==0)
{
//子进程调用execv后进程空间将被ls命令覆盖
if(execv("/bin/ls",param)<0)
perror("execv");
}
}

7.3.3 调用system创建进程
        为了方便地调用外部程序,Linux提供了system系统调用。与exec系统调用不同,system将外部可执行程序加载执行完毕后继续返回调用进程。system的返回值就是被加载的程序的返回值。system系统调用的原型为:
#include <stdlib.h>
int system(const char *cmd);
参数说明:
1)cmd:shell命令行。
返回值:
1)-1:执行system失败。
2)127:在system的内部实现中,system首先fork子进程,然后调用exec执行新的shell,在shell中执行被加载的程序。如果在调用exec时失败,system将返回127。由于被加载的外部程序也可能返回127,因此,在system返回127时,最好判断一下errno。如果errno不为0,表明调用system失败;否则,调用system成功。
3)其他:执行system成功,返回值即为被加载的外部程序的返回值。

例7-8:编程实现调用执行“ls -l”命令输出当前目录的文件列表。
代码如下:
#include <stdio.h>
#include <stdlib.h>
void main()
{
//调用system执行“ls -l”,并输出system的返回值
printf("system return code=%d\n",system("ls -l"));
}
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

7.4 进程的终止  

2011-10-16 22:45:36|  分类: 《Linux编程从入|字号 订阅

7.4.1 exit函数
        进程执行完毕后,应该合理的终止,释放进程占用的资源。终止进程的方式有多种,可以是接收到其他进程发送的信号而被动终止进程,也可以是进程自己执行完毕后主动退出进程。本节介绍主动退出进程的exit函数。在Linux中,除调用exit可以结束进程外,还有另一个函数_exit也可以实现类似的功能。但是,由于_exit函数在退出时并不刷新带缓冲I/O的缓冲区,所以在使用带缓冲的I/O操作时,应该调用exit函数,而不是_exit。这两个函数的原型为:
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);
参数说明:
1)status:该参数指定进程退出时的返回值,该返回值可以在shell中通过“echo $?”命令查看,也可以通过system函数的返回值取得,还可以在父进程中通过调用wait函数获得。
注意事项:
1)通常进程返回0表示正常退出(如exit(0)),返回非零表示异常退出(如exit(1)/exit(-1))。

7.4.2 wait函数
        一个进程结束时将向其父进程发送SIGCLD信号,父进程可以忽略该信号或者安装信号处理函数处理该信号。而处理该信号通常需要调用wait系列函数。wait系列函数的作用是等待子进程的退出并获取子进程的返回值。通常情况下,wait函数是阻塞等待的,直到调用进程的某个子进程退出。wait系列函数的原型为:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid,int *status,int options);
参数说明:
1)status:用于保存子进程的结束状态。
2)pid:为欲等待的子进程识别码,其数值意义如下:
pid<-1:等待进程组识别码为 pid 绝对值的任何子进程;
pid=-1:等待任何子进程,相当于 wait();
pid=0:等待进程组识别码与目前进程相同的任何子进程;
pid>0:等待任何子进程识别码为 pid 的子进程。
3)options:该参数提供了一些额外的选项来控制waitpid,可有以下几个取值或它们的按位或组合:
0:不是用任何选项;
WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
WUNTRACED:若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。
返回值:
1)-1:调用失败。
2)其他:调用成功,返回值为退出的子进程ID。
注意事项:
1)Linux提供了多个宏以便从该结束状态中获取特定信息,具体信息如下:
WIFEXITED(status):如果若为正常结束子进程返回的状态,则为真;对于这种情况可执行WEXITSTATUS(status),取子进程传给exit或_eixt的低8位。
WEXITSTATUS(status):取得子进程 exit()返回的结束代码,一般会先用 WIFEXITED 来判断是否正常结束才能使用此宏。
WIFSIGNALED(status):若为异常结束子进程返回的状态,则为真;对于这种情况可执行WTERMSIG(status),取使子进程结束的信号编号。
WTERMSIG(status) :取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED 来判断后才使用此宏。
WIFSTOPPED(status) :若为当前暂停子进程返回的状态,则为真;对于这种情况可执行WSTOPSIG(status),取使子进程暂停的信号编号。
WSTOPSIG(status) :取得引发子进程暂停的信号代码,一般会先用 WIFSTOPPED 来判断后才使用此宏。

例7-9:编程实现监控子进程退出。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void CbSigCld(int signo)
{
pid_t pid;
int status;
//调用wait等待子进程退出
if((pid=wait(&status))!=-1)
printf("child process %d exit!\n",pid);
//判断子进程退出时是否有返回值
if(WIFEXITED(status))
//输出子进程的返回值
printf("child process return value=%d\n",WEXITSTATUS(status));
//判断子进程是否被信号中断而结束
if(WIFSIGNALED(status))
//输出中断子进程的信号
printf("child process if terminated by signal %d\n",WTERMSIG(status));
}
void main()
{
pid_t pid;
//安装SIGCLD信号处理函数
signal(SIGCLD,CbSigCld);
//创建子进程
if((pid=fork())<0)
{
perror("fork");
return;
}
//子进程
if(pid==0)
{
exit(123);
}
//父进程休眠1秒,等待子进程退出。
sleep(1);
}
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

8. 信号  

2011-10-17 19:15:30|  分类: 《Linux编程从入|字号 订阅

导读:由于本章篇幅较长,所以把它拆分为如下几节。
8.1 信号的基本概念
8.2 信号的安装
8.3 信号的处理
8.4 信号的发送
8.5 两个重要信号
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

8.1 信号的基本概念  

2011-10-22 00:19:40|  分类: 《Linux编程从入|字号 订阅

8.1.1 信号的定义
        信号(signal)又称为软中断,用来通知进程发生了异步事件。信号的接收过程是异步的过程,也就是说,进程在正常运行过程中,随时可能被各种信号所中断。进程可以忽略该信号,也可以中断当前执行的程序转而调用相应的函数去处理信号。待信号处理完毕,继续执行被中断的程序。

8.1.2 信号的来源
        只要具备相应权限,进程之间可以相互通过系统调用kill发送信号。操作系统内核也可能因为内部事件而给进程发送信号,通知进程有某个事件发生。因此,信号的来源可能是系统内核,也可能是其他的进程。引起信号产生的原因基本上包括以下几种。
1)程序中执行错误的代码。如内存访问越界、数学运算除零等。
2)其他进程发送来的信号。
3)用户通过控制终端发送来的信号。最常见的情况是:一个程序在运行中,用户通过键盘输入<Ctrl>+<C>键或者<Ctrl>+<\>键终止程序的执行。
4)子进程结束时向父进程发送的SIGCLD信号。
5)程序中设定的定时器产生的SIGALRM信号。

8.1.3 信号的分类
        为方便地标识每一种信号,Linux系统为每种信号都分配了名字。这些名字都以SIG开头进行定义。可以通过执行命令kill -l获得系统中全部信号的列表,在Ubuntu-10.10系统中,信号列表如下所示。
 1) SIGHUP  2) SIGINT  3) SIGQUIT 4) SIGILL 5) SIGTRAP
 6) SIGABRT  7) SIGBUS  8) SIGFPE 9) SIGKILL10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE14) SIGALRM15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT19) SIGSTOP20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG24) SIGXCPU25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH29) SIGIO30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+136) SIGRTMIN+237) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+641) SIGRTMIN+742) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+1146) SIGRTMIN+1247) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-1451) SIGRTMAX-1352) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-956) SIGRTMAX-857) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-461) SIGRTMAX-362) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
        上面这些信号的定义在头文件<asm/signal.h>中。Linux系统中的信号可以划分为可靠信号和不可靠信号。可靠信号是指信号一旦发出,操作系统保证该信号不会丢失;而不可靠信号由于内核不对信号进行排队,造成的后果就是信号有可能丢失。在Linux的信号中,编号值是1~31(SIGHUP~SIGSYS)的信号是不可靠信号,而编号为32~64的信号是可靠信号。

8.1.4 常见信号
        Linux系统中常见的信号说明如下:
1)SIGHUP:该信号是在进程的控制终端注销时产生的,有系统内核发往该终端上运行的所有进程。用户在登录Linux系统时,系统将会分配给该用户一个控制终端(dev/tty1、/dev/pts/0等),所有在此终端上启动的用户进程,其控制终端就是该终端。如果此时用户从该终端注销,那么所有已启动的进程都将收到SIGHUP信号。在系统缺省状态下对该信号的处理就是中止进程。
2)SIGINT:程序终止信号。在程序运行过程中,用户通过键盘按下【Ctrl】+【C】键将产生该信号。
3)SIGQUIT:程序退出信号。在程序运行过程中,用户通过按下【Ctrl】+【\】键将产生该信号。与SIGINT不同的是SIGQUIT信号将产生系统核心转储core文件。core文件是程序退出时的内存映像文件,包含了与程序相关的调试信息。
4)SIGBUS和SIGSEGV:进程访问非法地址时,将引发该信号。如果地址是有效的,不过不属于当前进程的地址空间,则引发SIGSEGV信号。而如果地址本身是无效的,将引发SIGBUS信号。
5)SIGFPE:进行算术运算中出现致命错误,如除零操作、数据溢出等。
6)SIGKILL:终止用户进程执行的信号。该信号不能被忽略或者被用户捕获。一旦收到该信号,进程将立即中止。在shell下通过执行“kill -9”命令发送的就是该信号。
7)SIGTERM:进程结束信号。与SIGKILL不同的是,该信号可以被用户捕获并处理。在shell下执行“kill 进程pid”命令发送的就是该信号。
8)SIGALRM:定时器信号。可以在程序中通过alarm系统调用设置一个超时时钟,在设定的时间到达后,进程将收到该信号。
9)SIGCLD:子进程退出信号。在一个进程创建一个或者若干个子进程后,每个子进程退出时,父进程都将收到该信号。如果父进程没有忽略该信号,也没有处理该信号,则子进程退出后将形成僵尸进程。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

8.2 信号的安装  

2011-10-22 21:09:19|  分类: 《Linux编程从入|字号 订阅

8.2.1 用signal安装信号
        在用户的应用程序中,可以自行安装信号,定义进程收到信号后的处理方法。这一过程是通过系统调用signal或者sigaction完成的。本节介绍signal系统调用,下一节介绍sigaction系统调用。signal系统调用的原型为:
#include <signal.h>
__sighandler_t signal (int sig, __sighandler_t handler);
参数说明:
1)sig:要安装的信号值。
2)handler:指向信号处理函数的指针。除自行指定处理函数指针外,该参数还可以有以下选择。
SIG_DFN:采用缺省的信号处理方式;
SIG_IGN:忽略该信号。
返回值:
若成功,返回信号处理函数的指针;若失败,返回SIG_ERR。

例8-1:用signal为进程安装SIGINT信号。
代码如下:
#include <stdio.h>
#include <signal.h>
void CbSignal(int signo)
{
//输出接收到的信号信息
printf("\nreceive signal=%d.\n",signo);
}
void main()
{
//安装SIGINT信号
if(signal(SIGINT,CbSignal)==SIG_ERR)
{
perror("signal");
                return;
}
//提示用户输入通过<Ctrl>+<C>中止程序执行
printf("waiting for <Ctrl>+<C>\n");
//暂停,等待信号
pause();
}
编译:gcc signaltest.c -o signaltest
运行:./signaltest
输入:同时按下<Ctrl>+<C>组合键中止程序运行。注意观察控制台输出。

8.2.2 用sigaction安装信号
        signal可以实现基本的信号安装功能,但在某些情况下,可能需要对信号的安装进行更多的控制。此时就需要使用sigaction系统调用。sigaction的原型为:
#include <signal.h>
int sigaction(int sig,const struct sigaction *act,struct sigaction *oact);
参数说明:
1)sig:要安装的信号值。
2)act:指定安装信号的数据结构。该参数是一个struct sigaction类型的指针。该结构的定义位于头文件<bits/sigaction>中,如下所示。
struct sigaction
{
    /* Signal handler.  */
#ifdef __USE_POSIX199309
union
{
/* Used if SA_SIGINFO is not set.  */
__sighandler_t sa_handler;
/* Used if SA_SIGINFO is set.  */
void (*sa_sigaction) (int, siginfo_t *, void *);
}
    __sigaction_handler;
# define sa_handler__sigaction_handler.sa_handler
# define sa_sigaction__sigaction_handler.sa_sigaction
#else
    __sighandler_t sa_handler;
#endif
    /* Additional set of signals to be blocked.  */
    __sigset_t sa_mask;
    /* Special flags.  */
    int sa_flags;
    /* Restore handler.  */
    void (*sa_restorer) (void);
};
3)oact:输出参数,指向struct sigaction结构的指针。调用sigaction成功后,该参数中将返回信号的原来处理方式。如果不需要获取信号的原来处理方式,则该参数可以传入NULL。
返回值:
若成功,返回0;若失败,返回-1。
注意事项:
1)用signal安装的信号,无法在发送信号时附加其他数据。而sigaction函数通过SA_SIGINFO标志安装的信号,则可以实现这一点。

例8-2 用sigaction为进程安装SIGINT信号,要求在进程发送信号时可以附加数据。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
//信号处理函数
void CbSignal(int signo,siginfo_t *info,void *none)
{
int param;
param=info->si_value.sival_int;
printf("\nreceive signal=%d, additional data=%d\n",signo,param);
}
//主函数
void main()
{
//信号安装时需要用到的变量
struct sigaction act;
//清零
memset(&act,0,sizeof(act));
//清除信号处理掩码
sigemptyset(&act.sa_mask);
//设置信号处理函数
act.sa_sigaction=CbSignal;
//设置发送信号时可以附加数据
act.sa_flags=SA_SIGINFO;
//安装信号
if(sigaction(SIGINT,&act,NULL)<0)
{
//错误处理
perror("sigaction");
return;
}
//提示用户输入通过<Ctrl>+<C>中止程序执行
printf("waiting for <Ctrl>+<C>\n");
//暂停,等待信号
pause();
}

编译:gcc sigactiontest.c -o sigactiontest
运行:./sigactiontest
输入:同时按下<Ctrl>+<C>组合键中止程序运行。注意观察控制台输出。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

8.3 信号的处理  

2011-11-01 22:10:10|  分类: 《Linux编程从入|字号 订阅

8.3.1 信号的处理方式
        在Linux系统下,信号的处理方式有3种:一是忽略信号;而是按照系统提供的缺省处理规则进行处理;三是捕捉信号,在程序中定义自己的信号处理函数,在信号处理函数中完成相应的功能。Linux系统分别为前两种方式提供了相应的宏定义:SIG_IGN和SIG_DFN。下面列出几个重要信号及其处理方式。
-----------------------------------------------------------------------------------------------------------------
信号名称          缺省处理方式                                信号产生原因/说明
-----------------------------------------------------------------------------------------------------------------
SIGHUP          终止进程                                        控制终端挂起或者退出
SIGINT            终止进程                                        <Ctrl>+<C>
SIGQUIT         终止进程并进行内核映像转储         <Ctrl>+<\>
SIGKILL          终止进程                                        不能阻塞、忽略、捕捉
SIGALRM        终止进程                                        定时器超时信号 
SIGTERM        终止进程                                        可以阻塞、捕捉
SIGCHLD        混略信号                                        子进程退出时向父进程发送该信号
SIGSTOP        暂停进程执行                                 不能阻塞、忽略、捕捉
-----------------------------------------------------------------------------------------------------------------

8.3.2 信号的阻塞处理
        信号的阻塞就是通知系统内核暂时停止向进程发送指定的信号,而是由内核对进程接收到的相应信号进行缓存排队,直到进程解除对相应信号的阻塞为止。一旦进程解除对该信号的阻塞,则缓存的信号将被发送到相应的进程。
        信号在几种情况下会进入阻塞状态。
1)系统自动阻塞:在信号的处理函数执行过程中,该信号将被阻塞,直到信号处理函数执行完毕,该阻塞将会解除。这种机制的作用主要是避免信号的嵌套。
2)通过sigaction实现人为阻塞:在使用sigaction安装信号时,如果设置了sa_mask阻塞信号集,则该信号集中的信号在信号处理函数执行期间将会阻塞。这种情况下进行信号阻塞的主要原因是:一个信号处理函数在执行过程中,可能会有其他信号到来。此时,当前的信号处理函数就会被中断。而这往往是不希望发生的。此时,可以通过sigaction系统调用的信号阻塞掩码对相关信号进行阻塞。通过这种方式阻塞的信号,在信号处理函数执行结束后就会解除。
3)通过sigprocmask实现人为阻塞:可以通过sigprocmask系统调用指定阻塞某个或者某几个信号。这种情况下进行信号阻塞的原因较多,一个典型的情况是:某个信号的处理函数与进程某段代码都要某个共享数据区进行读写。如果当进程正在读写共享数据区的过程中,一个信号过来,则进程的读写过程将被中断转而执行信号处理函数,而信号处理函数也要对该共享数据区进行读写,这样共享数据区就会发生混乱。这种情况下,需要在进程读写共享数据区前阻塞该信号,在读写完成后再解除该信号的阻塞。
        提示:在信号的接收过程中可能存在这样的情况:若干个相同的信号同时到达。通过上面的介绍可以知道,当信号处理函数正在执行时,同类信号将被阻塞处理。但是,如果此时信号处理函数还没有来得及执行,那么该同类信号就不会阻塞,在这种情况下,将会发生一种成为“ 信号合并”的现象。同时到达的同类信号将被合并处理,就像只有一个信号到达一样。
        被阻塞的信号的集合成为当前进程的信号掩码。每个进程都有惟一的信号掩码。为了对信号进行阻塞或者解除阻塞,Linux提供了专门的系统调用sigprocmask完成这一任务。该函数的原型为:
#include <signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
参数说明:
1)how:输入参数,设置信号阻塞掩码的方式。可以包括3种方式对信号的掩码进行设置,分别是阻塞信号的SIG_BLOCK、解除阻塞的SIG_UNBLOCK和设置阻塞掩码的SIG_SETMASK。
2)set:输入参数,阻塞信号集。当参数how为SIG_BLOCK时,该参数表明要阻塞的信号集;当how参数为SIG_UNBLOCK时,该参数表明要解除阻塞的信号集;当how参数为SIG_SETMASK时,该参数表明要阻塞的信号集。
3)oset:输出参数,原阻塞信号集。
返回值:
若成功,返回0;若失败,返回-1。

例8-3:编程实现下面功能:为进程安装SIGINT信号,先阻塞该信号,休眠10秒,再解除该信号的阻塞。
代码如下:
#include <stdio.h>
#include <signal.h>
//SIGINT信号处理函数
void CbSigInt(int signo)
{
//输出信号的值
printf("receive signal %d\n",signo);
}
void main()
{
//信号掩码结构变量,用于指定新的信号掩码
sigset_t mask;
//信号掩码结构变量,用于保存原来的信号处理掩码
sigset_t omask;
//安装SIGINT信号
signal(SIGINT,CbSigInt);
//清空信号掩码变量
sigemptyset(&mask);
//向掩码结构中增加信号SIGINT
sigaddset(&mask,SIGINT);
//阻塞SIGINT信号
sigprocmask(SIG_BLOCK,&mask,&omask);
//休眠10秒
sleep(10);
//解除SIGINT信号的阻塞
sigprocmask(SIG_SETMASK,&omask,NULL);
}
编译运行该程序,在进程休眠期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,注意观看信号是否被阻塞。在休眠结束后,验证刚才被阻塞的SIGINT信号是否被重新发送。

提示:在创建新的子进程时,子进程将继承父进程的信号掩码。

8.3.3 信号集的操作
        通过上节对信号阻塞的介绍可以知道,信号的阻塞实际上是对一个集合的操作。这个集合中可能包含多种信号,这就是信号集。信号集的数据类型为sigset_t,实际上是个结构体,它的定义如下所示。
typedef struct
{
unsigned long sig[_NSIG_WORDS];
}sigset_t;
        Linux系统提供了一系列函数对信号集进行操作。这些函数的原型如下所示。
#include <signal.h>
sigemptyset(sigset_t *set); //初始化由set指定的信号集,信号集里面的所有信号被清空;
sigfillset(sigset_t *set); //调用该函数后,set指向的信号集中将包含linux支持的64种信号;
sigaddset(sigset_t *set,int signo);                 //在set指向的信号集中加入signo信号;
sigdelset(sigset_t *set,int signo);                  //在set指向的信号集中删除signo信号;
sigismember(const sigset_t *set,int signo);     //判定信号signo是否在set指向的信号集中。
参数说明:
1)set:输入参数,信号集。
2)signo:输入参数,要增加或删除或判断的信号。
返回值:
1)对于sigismember函数:返回1表示信号属于信号集;返回0表示信号不属于信号集。
2)对于其他函数:若成功,返回0;若失败,返回-1。

8.3.4 未决信号的处理
        信号的未决是信号产生后的一种状态,是指从信号产生后,到信号被接收进程处理之前的一种过渡状态。由于信号的未决状态时间非常短,所以通常情况下,处于未决状态的信号非常少。如果程序中使用了sigprocmask阻塞了某种信号,则向进程发送的这种信号将处于未决状态。Linux提供了专门的函数sigpending获取当前进程中处于未决状态的信号。该函数的原型为:
#include <signal.h>
int sigpending(sigset_t *set);
参数说明:
1)set:输出参数,处于未决状态的信号集。
返回值:
若成功,返回0;若失败,返回-1。

例8-4:编程实现下面功能:为进程安装SIGINT信号,先阻塞该信号,休眠10秒,最后查看当前进程未决的信号。
代码如下:
#include <stdio.h>
#include <signal.h>
void main()
{
//信号掩码结构变量,用于指定新的信号掩码
sigset_t mask;
//信号掩码结构变量,用于保存原来的信号处理掩码
sigset_t omask;
//信号掩码结构变量,用于保存未决的信号集
sigset_t pendmask;
//清空信号掩码变量
sigemptyset(&mask);
//向掩码结构中增加信号SIGINT
sigaddset(&mask,SIGINT);
//阻塞SIGINT信号
sigprocmask(SIG_BLOCK,&mask,&omask);
//休眠10秒
sleep(10);
//获取当前未决的信号集
if(sigpending(&pendmask)<0)
{
perror("sigpending");
//解除SIGINT信号的阻塞
sigprocmask(SIG_SETMASK,&omask,NULL);
return;
}
//判断SIGINT是否在未决信号集中
if(sigismember(&pendmask,SIGINT))
printf("SIGINT signal is pending.\n");
else
printf("SIGINT signal is not pending.\n");
//解除SIGINT信号的阻塞
sigprocmask(SIG_SETMASK,&omask,NULL);
}
编译运行该程序,在进程休眠期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证该信号是否处于未决状态。

8.3.5 等待信号

8.3.5.1 pause
        在有些情况下,程序需要暂停执行,进入休眠状态,以等待信号的到来。这时可以使用pause系统调用。pause一旦被调用,则进程将进入休眠状态。之后,只有在进程接收到信号后,pause才会返回。pause的原型为:
#include <unistd.h>
int pause();
返回值:
pause的返回值永远是-1,错误码errno为EINTR。

例8-5:用pause编程实现等待SIGINT信号到来的功能。
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
//SIGINT信号处理函数
void CbSigInt(int signo)
{
//输出信号的值
printf("receive signal %d\n",signo);
}
void main()
{
//安装SIGINT信号
signal(SIGINT,CbSigInt);
//等待信号
pause();
}
编译运行该程序,在进程pause期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证该信号是否被处理。
注意:由于此测试程序没有屏蔽其他信号,因此任何一个信号的到来都能唤醒pause。

8.3.5.2 sigsuspend
        pause系统调用可以实现暂停进程的执行等待某个信号的到来,但是,如果在pause被调用之前,指定的信号到达进程,那么,在随后的pause调用中,假定不再有信号到来,则进程将进入无限期的等待中。为此Linux提供了功能更强大的sigsuspend以满足这种需求。sigsuspend的工作过程如下:
1)设置进程的信号掩码并阻塞进程。
2)收到信号,恢复原来的信号掩码。
3)调用进程设置的信号处理函数。
4)等待信号处理函数返回后,sigsuspend返回。
        上述四个步骤是一次性完成的,操作系统保证操作过程的原子性。特别需要注意的是第三步调用信号处理函数是由sigsuspend完成的。sigsuspend的原型为:
#include <signal.h>
int sigsuspend(const sigset_t *set);
参数说明:
1)set:输入参数,执行sigsuspend过程中需要被阻塞的信号集。
返回值:
sigsuspend的返回值永远是-1,错误码errno为EINTR。

例8-6:用sigsuspend编程实现等待SIGINT信号到来的功能。
代码如下:
#include <stdio.h>
#include <signal.h>
//SIGINT信号处理函数
void CbSigInt(int signo)
{
//输出信号的值
printf("receive signal %d\n",signo);
}
void main()
{
//信号掩码结构变量,用于指定新的信号掩码
sigset_t mask;
//安装SIGINT信号
signal(SIGINT,CbSigInt);
//设置信号集为所有信号,准备阻塞所有信号
sigfillset(&mask);
//从信号集中删除SIGINT信号,该信号为目标信号,不能被阻塞。
sigdelset(&mask,SIGINT);
//等待SIGINT信号
sigsuspend(&mask);
}
编译运行该程序,在进程suspend期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证该信号是否被处理。
注意:由于此测试程序屏蔽了除SIGINT外的所有其他信号,因此只有在收到SIGINT信号后进程才会退出。

提示:
        一个阻塞式系统调用在执行过程中如果没有符合条件的数据,将进入休眠状态,直到有符合条件的数据到来。比较典型的例子是从网络连接上读取数据,如果没有数据到来,那么这个读操作将会阻塞。此时有两种情况可以中断该读操作的执行:一是网络上有数据到来,则读操作将获取到所需要的数据后返回;二是当前进程收到了某个信号,此时,读操作将被中断并返回失败,错误码errno为EINTR。

8.3.6 信号处理函数的实现
        信号处理函数是进程接收到信号后要执行的函数,该函数应该尽量简洁,一般不要执行过多的代码。最好只是改变一个外部标志变量的值,而在另外的程序中不断的检测该变量,繁杂的工作都留给那些程序去做。在定义信号处理函数时,应该特别注意以下几点。
1)如果信号处理程序中需要存取某个全局变量,则应该在程序中使用关键字volatile声明此变量。通知编译器,在编译过程中不要对该变量进行优化。
2)如果在信号处理函数中调用某个函数,那么那么该函数必须是可重入的,或者保证在信号处理函数执行期间不会有信号到达进程。Linux系统下存在许多不可重入的函数,如malloc、gethostbyname等。
        在信号处理函数里,有时需要用到长跳转的操作。所谓长跳转,就是从信号处理函数直接跳转到函数体外指定的代码位置继续运行。Linux系统提供了两个函数实现该功能:设置跳转点的sigsetjmp和执行跳转的siglongjmp。sigsetjmp用来设置跳转点,在成功调用后,sigsetjmp语句所在的位置就是跳转点,这个位置指针将被保存到sigsetjmp的第一个参数中。这个两个函数的原型为:
#include <setjmp.h>
int sigsetjmp (struct __jmp_buf_tag env[1], int savemask);
void siglongjmp(sigjmp_buf env,int val);
参数说明:
1)env[1]:输出参数,该参数实际上是一个结构体的指针。该结构体中包含了长跳转指针,是否保存信号掩码及保存的信号掩码值等信息。对于应用人员来说,该结构是透明的。
2)env:输入参数,等效于env[1]。
3)savemask:是否保存信号掩码。如果该参数非零,则在调用sigsetjmp后,当前进程的信号掩码将被保存;在调用siglongjmp时,将恢复由sigsetjmp保存的信号掩码。
4)val:当由siglongjmp调用sigsetjmp时,该参数将会被隐含传给sigsetjmp作为返回值。如果val等于0,那么sigsetjmp函数将忽略该参数而返回其他非零值。
返回值:
1)sigsetjmp函数:若返回0,表明sigsetjmp不是由siglongjmp调用的;若返回非零值,则是由siglongjmp调用而返回。

例8-7:编程实现捕捉SIGINT信号,在信号处理函数中用长跳转跳转至主程序。
代码如下:
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
//全局变量,用于保存跳转点及其现场
static sigjmp_buf jmpbuff;
//SIGINT信号处理函数
void CbSigInt(int signo)
{
//输出信号的值
printf("\nreceive signal %d\n",signo);
//长跳转到jmpbuff(即sigsetjmp函数入口处),并平衡堆栈
siglongjmp(jmpbuff,88);
}
void main()
{
int res;
//安装SIGINT信号
signal(SIGINT,CbSigInt);
//设置跳转点
res=sigsetjmp(jmpbuff,1);
//第一次调用sigsetjmp时将返回0
if(res==0)
printf("First call sigsetjmp!\n");
//从信号处理函数中跳转过来时,sigsetjmp将返回非零值
else
{
//输出提示信息后退出进程
printf("res=%d\n",res);
printf("sigsetjmp is called by siglongjmp!\n");
return;
}
//暂停执行等待信号
pause();
}

编译运行该程序,在进程pause期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证信号处理函数是否跳转到指定的位置。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

8.4 信号的发送  

2011-11-07 23:43:55|  分类: 《Linux编程从入|字号 订阅

        信号的来源很多,可能是由硬件产生,也可能是由其他用户进程发送过来。本节主要介绍如何在某个进程中向另一个进程发送信号。

8.4.1 使用kill发送信号
        kill系统调用可以实现信号的发送,它既可以向单个进程发送信号,也可以向一组进程发送信号。kill的原型为:
#include <signal.h>
int kill(pid_t pid,int signo);
参数说明:
1)pid:目标进程ID。通过传递不同pid值,kill可以实现多种信号发送方式。pid主要取值如下。
pid>0:将信号传给进程识别码为pid 的进程。
pid=0:将信号传给和目前进程相同进程组的所有进程
pid=-1:将信号广播传送给系统内所有的进程
pid<0:将信号传给进程组识别码为pid绝对值的所有进程
2)signo:信号值。
返回值:
若成功,返回0;若失败,返回-1。

提示:除了kill外,Linux还提供了raise函数用于发送信号。不过,raise只能用于向进程自身发送信号。raise函数的功能可以通过kill(getpid(),SIGXXX)替代实现。

例8-8:编写两个程序:killrecerver和killsender。其中killrecerver用于接收SIGUSR1(值为10)信号,而killsender通过kill系统调用向指定进程(pid)发送信号。
killrecerver代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
//SIGUSR1信号处理函数
void CbSigUsr1(int signo)
{
//输出接收到的信号信息
printf("\nreceive signal=%d.\n",signo);
}
void main()
{
//安装SIGUSR1信号
if(signal(SIGUSR1,CbSigUsr1)==SIG_ERR)
{
perror("signal");
return;
}
//输出当前进程的PID
printf("my pid is %d\n",getpid());
//输出提示信息
printf("waiting for SIGUSR1...\n");
//暂停,等待信号
pause();
}

killsender代码如下:
#include <stdio.h>
#include <signal.h>
void main(int argc,char *argv[])
{
int pid,signo;
//判断命令行参数
if(argc!=3)
{
printf("usage: killsender pid signo\n");
return;
}
//获取参数pid
sscanf(argv[1],"%d",&pid);
//获取参数signo
sscanf(argv[2],"%d",&signo);
//发送信号
if(kill(pid,signo)<0)
{
perror("kill");
return;
}
//输出提示信息
printf("ok: send out signal via kill system call!\n");
}

说明:
1)编译分别生成程序killreceiver和killsender。
2)在命令台先启动killreceiver,并让它在后台运行: ./killreceiver&
此时killreceiver会输出自己的进程pid。
3)执行killsender,参数为killreceiver的pid和10(信号SIGUSR1)。
4)注意观察控制台输出结果。

8.4.2 使用sigqueue发送信号
        通过kill可以实现信号的发送。有些情况下需要在发送信号的同时附加其他数据,这时kill无法满足要求。Linux为此而提供了另一个系统调用sigqueue。该系统调用不仅可以实现kill的全部功能,而且还可以实现支持附加数据的发送。sigqueue的原型为:
#include <signal.h>
int sigqueue(pid_t pid,int signo,const union sigval val);
参数说明:
1)pid:目的进程ID。与kill不同,sigqueue只能向一个进程发送信号。
2)signo:要发送的信号值。
3)val:信号的附加数据。该参数是一个共用体,其定义如下:
typedef union sigval
{
    int sigval_int;
    void *sival_ptr;
}sigval_t;
在调用sigqueue时,该参数将会被复制到信号处理函数的第二个参数中。在信号处理函数中可以获取到附加数据并进行处理。
返回值:
若成功,返回0;若失败,返回-1。

例8-9:编写两个程序:sigrecerver和sigsender。其中sigrecerver用于接收SIGUSR1(值为10)信号,而sigsender通过sigqueue系统调用向指定进程(pid)发送信号。在发送信号时附加额外数据。
sigreceiver代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
//信号处理函数
void OnSigUsr1(int signo,siginfo_t *info,void *none)
{
int data;

//输出接收到的信号值
printf("\nreceive signal=%d\n",signo);
data=info->si_value.sival_int;
printf("receive additional data=%d\n",data);
}
//主函数
void main()
{
//信号安装时需要用到的变量
struct sigaction act;

//清零
memset(&act,0,sizeof(act));
//清除信号处理掩码
sigemptyset(&act.sa_mask);
//设置信号处理函数
act.sa_sigaction=OnSigUsr1;
//设置发送信号时可以附加数据
act.sa_flags=SA_SIGINFO;
//安装信号
if(sigaction(SIGUSR1,&act,NULL)<0)
{
//错误处理
perror("sigaction");
return;
}
//输出当前进程的PID
printf("\nmy pid is %d\n",getpid());
//输出提示信息
printf("waiting for SIGUSR1...\n");
//暂停,等待信号
pause();
}

sigsender代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
//主函数
void main(int argc,char* argv[])
{
int pid,signo,data;
//发送信号的附加数据时需要用到的变量
union sigval sigparam;
//判断命令行参数个数
if(argc!=4)
{
//如果参数不对则输出提示信息,然后进程结束
printf("usage: sigsender pid signo data\n");
return;
}
//获取命令行参数
sscanf(argv[1],"%d",&pid);
sscanf(argv[2],"%d",&signo);
sscanf(argv[3],"%d",&data);
//设置附加数据
sigparam.sival_int=data;
//发送信号SIGUSR1
if(sigqueue(pid,SIGUSR1,sigparam)<0)
{
//错误处理
perror("sigqueue");
}
//输出提示信息
printf("ok: send out signal via sigqueue system call!\n");
}

说明:
1)编译分别生成程序sigreceiver和sigsender。
2)在命令台先启动sigreceiver,并让它在后台运行: ./sigreceiver&
此时sigreceiver会输出自己的进程pid。
3)执行sigsender,参数为sigreceiver的pid、10(信号SIGUSR1)和附加数据。
4)注意观察控制台的输出结果。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

8.5 两个重要信号  

2011-11-09 00:22:27|  分类: 《Linux编程从入|字号 订阅

8.5.1 SIGALRM信号
        在进行阻塞式系统调用时,为避免进程陷入无限期的等待,可以为这些阻塞式系统调用设置定时器。Linux提供了alarm系统调用和SIGALRM信号实现这个功能。
        要使用定时器,首先要安装SIGALRM信号。如果不安装SIGALRM信号,则进程收到SIGALRM信号后,缺省的动作就是终止当前进程。SIGALRM信号安装成功后,在什么情况下进程会收到该信号呢?这就要依赖于Linux提供的定时器功能。在Linux系统下,每个进程都有惟一的一个定时器,该定时器提供了以秒为单位的定时功能。在定时器设置的超时时间到达后,调用alarm的进程将收到SIGALRM信号。alarm系统调用的原型为:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数说明:
1)seconds:要设定的定时时间,以秒为单位。在alarm调用成功后开始计时,超过该时间将触发SIGALRM信号。
返回值:
返回当前进程以前设置的定时器剩余秒数。

例8-10:编程利用SIGALRM信号实现秒定时器。
代码如下:
#include <stdio.h>
#include <signal.h>
//全局计数器变量
int Cnt=0;
//SIGALRM信号处理函数
void CbSigAlrm(int signo)
{
//输出定时提示信息
printf("   seconds: %d",++Cnt);
printf("\r");
//重新启动定时器,实现1秒定时
alarm(1);
}
void main()
{
//安装SIGALRM信号
if(signal(SIGALRM,CbSigAlrm)==SIG_ERR)
{
perror("signal");
return;
}
//关闭标准输出的行缓存模式
setbuf(stdout,NULL);
//启动定时器
alarm(1);
//进程进入无限循环,只能手动终止
while(1)
{
//暂停,等待信号
pause();
}
}

8.5.2 SIGCLD信号
        在Linux的多进程编程中,SIGCLD是一个非常重要的信号。当一个子进程退出时,并不是立即释放其占用的资源,而是通知其父进程,由父进程进行后续的工作。在这一过程中,系统将依次产生下列事件。
1)向父进程发送SIGCLD信号,子进程进入zombie(僵尸)状态。
2)父进程接收到SIGCLD信号,进行处理。
        如果在上述过程中父进程既没有忽略SIGCLD信号,也未捕获该信号进行处理,则子进程将进入僵尸状态。僵尸状态的进程不能被操作系统调用,也没有任何可执行代码,它不过是占用了进程列表中的一个位置而已。如果仅有几个僵尸进程不会影响系统的运行,但是如果僵尸进程过多,则将会严重影响系统的运行。因此,在编程过程中应避免产生僵尸进程。有两种基本的处理方法可以避免产生僵尸进程:一是父进程忽略SIGCLD信号;二是父进程捕获SIGCLD信号,在信号处理函数中获取子进程的退出状态。忽略信号的方式比较简单,只需要调用 signal(SIGCLD,SIG_IGN)语句即可完成。如果要捕获信号并处理,那么先要安装SIGCLD信号,然后在信号处理函数中调用wait或者waitpid等函数获取子进程的退出状态。

例8-11:编程捕获SIGCLD信号,输出各子进程的ID和退出状态码。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
//SIGCLD信号处理函数
void CbSigCld(int signo)
{
//保存退出进程的ID
int pid;
//保存退出进程的退出状态码
int status;
//等待任何一个子进程退出
pid=waitpid(-1,&status,0);
//输出退出的子进程ID和退出代码
printf("Child process %d exit with status %d\n",pid,status);
}
void main()
{
int i,pid;
//安装SIGCLD信号
if(signal(SIGCLD,CbSigCld)==SIG_ERR)
{
perror("signal");
return;
}
//循环创建子进程
for(i=0;i<5;i++)
{
pid=fork();
//如果是子进程
if(pid==0)
{
//退出子进程,退出状态码为0
exit(0);
}
//如果是父进程
else
{
sleep(1);
}
}
}
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

9. 进程间通信之管道  

2011-11-09 23:36:20|  分类: 《Linux编程从入|字号 订阅

导读:由于本章篇幅较长,所以把它拆分为如下几节。
9.1 管道的基本概念
9.2 普通管道编程
9.3 命名管道编程
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

9.1 管道的基本概念  

2011-11-13 18:13:41|  分类: 《Linux编程从入|字号 订阅

9.1.1 管道的概念
        管道,顾名思义就是一个连接两个进程的连接器。管道是单向的,一端只能用于输入,另一端只能用于输出。管道的输出遵循“先进先出(FIFO)”的原则。向管道中写入的数据附加至管道缓冲区的尾部,而读取数据则是从管道缓冲区的头部开始。
        以统计当前目录下文件数量为例,可以用shell命令来实现,如:“ls|wc -l”,中间的“|”在Linux的shell中就是管道符号。这条命令的含义是“ls”的输出通过管道作为“wc -l”命令的输入。如此一来,ls的输出不再在输出设备上显示,而是作为wc命令的输入。
        从本质上看,Linux管道也是一种文件,不过这种文件比较特殊,具有以下特点:
1)管道中的数据流是单向的,只能由写入端向读出端流动。
2)管道中的数据遵循“FIFFO”的原则,最先写入的数据一定被最先读出。
3)管道有大小限制,在Linux系统下,该大小为4096字节。在管道满时,写管道操作将阻塞;在管道空时,读管道操作将阻塞。

9.1.2 管道的分类
        管道可以分为普通管道和命名管道两种。普通管道提供了进程与其子进程进行通信的有效手段。通常情况下,父进程创建一个普通管道,并将该管道传递给子进程,这样,通过管道父子进程便可以方便地进行通信。命名管道在功能上与普通管道是相同的,只不过命名管道位于文件系统中,只要可以访问文件系统的进程都可以通过管道的名字进行访问。所以,命名管道可以实现无任何亲缘关系的两个进程间的通信。
        根据以上描述,可以总结出普通管道与命名管道的不同之处:
1)普通管道位于内存中,只能用于亲缘关系的进程间通信,或者用于同一个进程内的不同线程间通信。
2)命名管道位于文件系统中,可以实现有/无任何亲缘关系的进程或线程间通信。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

9.2 普通管道编程  

2011-11-14 22:21:15|  分类: 《Linux编程从入|字号 订阅

        主要的管道操作包括创建管道、读写管道、关闭管道等。

9.2.1 创建管道
        创建管道的函数是pipe,该函数以一个两个成员的整型数组为参数。调用成功后,数组的第一个成员是用于读的文件描述符,而数组的第二个成员是用于写的文件描述符。pipe的原型为:
#include <unistd.h>
int pipe(int pfds[2]);
参数说明:
1)pfds:整型数组,在函数执行成功后,该数组中将保存系统返回的管道描述符。其中pfds[0]用于读操作,而pfds[1]用于写操作。
返回值:
若成功,返回0;若失败,返回-1。

9.2.2 读写管道
        管道创建成功后,分别返回一个读管道描述符和一个写管道描述符。可以用文件操作的相关函数对描述符进行操作,如调用read从读描述符中读取数据;调用write向写描述符写入数据。进行读写过程中,应注意以下几点:
1)如果管道的写描述符关闭,则读函数返回的读出字节数为0。
2)如果管道的读描述符关闭,则写入操作将是无意义的。写操作将返回错误SIGPIPE。
3)向管道中写入时,如果管道已满,则写操作将阻塞直至管道有可用空间。
4)从管道中读出时,如果管道已空,则读操作将阻塞直至管道有可用的数据。

9.2.3 关闭管道
        在使用完管道后,应该调用close关闭管道。由于一个管道包含两个描述符,所以应该分别调用close进行关闭。另外,在多进程环境下,要注意管道描述符的引用计数,只有在引用计数为0的情况下,描述符才会真正被关闭。

例9-1:创建一个管道,将命令行参数通过管道传输至新派生的子进程中。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
void main(int argc,char *argv[])
{
//整型数组,用于保存打开管道后的两个文件描述符
int pfds[2];
//用于保存子进程ID
pid_t pid;
//临时字符变量
char ch;
//判断命令行参数是否符合要求
if(argc!=2)
{
//如果不符合要求则提示用法
printf("usage:%s <string>\n",argv[0]);
return;
}
//创建管道
if(pipe(pfds)<0)
{
perror("pipe");
return;
}
//创建子进程
pid=fork();
//如果创建进程失败
if(pid==-1)
{
perror("fork");
close(pfds[0]);
close(pfds[1]);
return;
}
//如果是子进程
if(pid==0)
{
//关闭管道的写描述符(实际上是将引用计数减一)
close(pfds[1]);
//从管道中循环读取数据
while(read(pfds[0],&ch,1)>0)
//输出读到的数据
printf("%c",ch);
printf("\n");
//关闭管道的读描述符(实际上是将引用计数减一)
close(pfds[0]);
}
//如果是父进程
else
{
//关闭管道的读描述符(实际上是将引用计数减一)
close(pfds[0]);
//向管道中写入数据(数据来自于命令行参数)
write(pfds[1],argv[1],strlen(argv[1]));
//关闭管道的写描述符(实际上是将引用计数减一)
close(pfds[1]);
//等待子进程的退出
wait(NULL);
}
}

编译链接,并在shell下执行该程序,如下:
./pipetest 123
运行结果为:
123

9.2.4 管道I/O
        在应用管道进行编程时,往往需要首先创建一个管道,然后并发一个进程,父子进程通过管道进行数据交换。为更方便地进行编程,Linux的标准I/O库提供了两个专用的I/O函数:popen和pclose。这两个函数的原型为:
#include <stdio.h>
FILE* popen(const char *cmdline,const char* modes);
int pclose(FILE *pfp);
参数说明:
1)cmdline:要执行的命令行。
2)mode:管道操作方式。该参数只能是读(r)或者写(w)中的一种,得到的返回值(标准 I/O 流)也具有和 mode 相应的只读或只写模式。如果 mode 是 "r" 则文件指针连接到 cmdline 的标准输出;如果 mode 是 "w" 则文件指针连接到 cmdline 的标准输入。
返回值:
若失败,返回NULL;若成功,返回非NULL值。
提示:
        从popen函数底层的实现来看,其实现的功能是:首先,创建一个管道,然后fork一个子进程,在子进程中通过exec系列函数加载要执行的程序(即popen函数的第一个参数)。popen调用成功后,将返回一个FILE*数据结构的指针,这是一个标准I/O文件指针。对该指针的解析要结合popen函数的第二个参数进行。如果调用popen时,指定的第二个参数是“r”,则该文件指针可用来读取命令(popen的第一个参数)的输出;如果指定的第二个参数是“w”,则该文件指针可用来向命令输入数据。

例9-2:通过popen编程统计输入数据的行数,输入行内容为exit时则结束。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void main()
{
//流文件指针
FILE *pfp;
//缓冲区
char buff[1024];
//调用popen执行“wc -l”命令,该命令的输入来自pfp
pfp=popen("wc -l","w");
//循环从键盘输入行数据,知道输入某行数据为exit则结束
while(1)
{
//清空缓冲区
memset(buff,0,sizeof(buff));
//从键盘接收输入数据
scanf("%s",buff);
//判断是否为退出命令
if(strcmp(buff,"exit")==0)
break;
//向pfp中写入数据,实际上是作为“wc -l”的输入
fprintf(pfp,"%s\n",buff);
}
//关闭流
pclose(pfp);
}

编译链接,并在shell下执行该程序,如下:
./popentest
aaa
bbb
123
exit
运行结果为:
3
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

9.3 命名管道编程  

2011-11-24 16:35:03|  分类: 《Linux编程从入|字号 订阅

        普通管道的缺点是只能在有亲缘关系的进程间通信。为解决这一问题,Linux提供了命名管道的概念。命名管道提供了一个文件名与管道关联。在命名管道创建成功后,文件系统中将产生一个物理的FIFO文件。对命名管道的操作,实际将通过该FIFO文件实现。通过这一机制,实现了无亲缘关系的进程也可以进行数据交换。与普通管道的操作相比,命名管道多了打开管道及删除管道的操作。

9.3.1 创建管道

9.3.1.1 通过shell命令创建
        在Linux下有两个shell命令可以用来创建命名管道,分别是mknod和mkfifo。语法分别如下:
1)mknod 管道名称 p
说明:参数p是向mknod指明新创建的节点类型是管道。
2)mkfifo -m 权限 管道名称

9.3.1.2 通过系统调用创建
        Linux提供了系统调用mkfifo用于创建命名管道。注意:这里的mkfifo是一个函数,与上述的shell命令是不同的。该系统调用的原型为:
#include <sys/stat.h>
int (__const char *path, __mode_t mode);
参数说明:
1)path:管道名称路径,类似于文件路径。可以使用绝对路径,也可以使用相对路径。
2)mode:新建管道的访问权限,与文件的访问权限相同,如0666。
返回值:
若成功,返回0;若失败,返回-1。

9.3.2 打开管道及读写
        管道创建成功后,即可以用它来进行交换,这时可以将它看作文件,使用文件操作函数进行操作。如,open、read、write、close等。

例9-3:编程实现命名管道的创建、打开、读写等操作。本例子包括两个程序wfifo和rfifo,其中wfifo以只写方式打开管道并向管道写入数据,而rfifo以只读方式打开管道并从管道中读取数据。
wfifo代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void main()
{
int pfd,n;
//创建管道
if(mkfifo("fifo1",0666)<0)
{
perror("mkfifo");
}
//打开管道
pfd=open("fifo1",O_WRONLY);
if(pfd<0)
{
perror("open");
return;
}
printf("ok: open fifo1 for write successful!\n");
//向管道写数据
n=write(pfd,"how are you?",12);
printf("ok: write data %d bytes to fifo1\n",n);
close(pfd);
}

rfifo代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void main()
{
int pfd,n;
char buff[100];
//创建管道
if(mkfifo("fifo1",0666)<0)
{
perror("mkfifo");
}
//打开管道
pfd=open("fifo1",O_RDONLY);
if(pfd<0)
{
perror("open");
return;
}
printf("ok: open fifo1 for read successful!\n");
//从管道中读数据
memset(buff,0,sizeof(buff));
n=read(pfd,buff,100);
printf("ok: read %d bytes from fifo1\n",n);
if(n>0)
{
printf("the data from fifo1: %s\n",buff);
}
close(pfd);
}

将这两个文件放到同一个目录,分别打开两个控制台编译它们,运行观察输出结果。

9.3.3 管道的删除
        与文件操作相同,命名管道的删除也可以通过unlink函数实现。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

10. 进程间通信之消息队列  

2011-11-28 17:56:41|  分类: 《Linux编程从入|字号 订阅

导读:由于本章篇幅较长,所以把它拆分为如下几节。
10.1 System V 进程间通信概述
10.2 消息队列的基本概念
10.3 消息队列编程
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

10.1 System V 进程间通信概述  

2011-12-18 22:20:37|  分类: 《Linux编程从入|字号 订阅

        Linux系统的IPC机制支持不同的标准,包括System V标准和POSIX标准。目前大量的应用都使用System V标准实现。System V进程间通信主要包括消息队列、共享内存、信号量,简称IPC。这3种方式的用途各不相同,实现机制则大同小异。IPC对象可以通过编程接口进行访问控制,同时也可以从shell环境中通过命令直接访问。

10.1.1 shell环境控制IPC
        IPC对象一经创建,系统内核即为该对象分配相关的数据结构。为方便对IPC对象的管理,Linux提供了专门的IPC控制命令,主要包括查看IPC对象信息的ipcs和删除IPC对象的ipcrm。

10.1.1.1 查看IPC对象信息
命令:ipcs [-aqms]
参数说明:
1)-a:查看全部IPC对象信息。
2)-q:查看消息队列信息。
3)-m:查看共享内存信息。
4)-s:查看信号量信息。

10.1.1.2 删除IPC对象
命令1:ipcrm -[qms] ID
命令2:ipcrm -[QMS] key
参数说明:
1)-q或-Q:删除消息队列信息。
2)-m或-M:删除共享内存信息。
3)-s或-S:删除信号量信息。
注意事项:
        如果指定了qms,则用IPC对象的标识符(ID)作为输入;如果指定了QMS,则用IPC对象的键值(key)作为输入。

10.1.2 进程将通信关键字
        与文件系统中的文件一样,为区别不同的进程间通信对象,Linux的每个IPC对象都有一个名字,成为“键”(key)。这个关键字是全局惟一的,类似于文件系统中的文件名称。
        进程间通信关键字是一个32位的长整型数据。在使用过程中,不同的进程直接使用key去创建IPC对象容易引起混淆,并且不同的应用之间可能会因为使用同一个key而产生冲突。为此,Linux系统提供了如下机制产生惟一的关键字。
1)创建IPC对象时,指定关键字为IPC_PRIVATE。通过该参数创建的IPC对象的关键字值是0,所以无法在其他进程中通过关键字对该对象进行访问,只能通过返回的标识符进行访问。
2)调用函数ftok产生一个惟一的关键字值。通过IPC进行通信的进程,只需要按照相同的参数调用ftok即可产生惟一的参数。通过该参数可有效解决关键字的产生及惟一性问题。

10.1.3 进程间通信标识符
        虽然IPC机制提供了“键”来惟一标识一个IPC对象,但是对IPC对象的访问并不是通过“键”,而是通过标识符进行的。IPC机制提供了相应的编程接口根据IPC键值获取标识符。对于一个IPC对象,在打开时返回一个标识符,而关闭后再次打开同一个IPC对象时,该标识符将顺序加1.这就是下面将要介绍的IPC权限许可结构中的seq字段的作用。

10.1.4 IPC权限许可结构
        在IPC对象创建成功后,Linux系统在内核为该对象分配了权限许可结构并初始化,该结构定义了对该IPC对象进访问的许可权。IPC对象创建后,可以通过相应的IPC控制函数(msgctl、shmctl、semctl等)对权限许可及属主ID等进行修改。这一过程类似于文件系统中的chmod和chown。该结构的定义位于头文件<bits/ipc.h>中,如下所示。
/* Data structure used to pass permission information to IPC operations.  */
struct ipc_perm
{
    __key_t __key;        /* Key.  */
    __uid_t uid;                 /* Owner's user ID.  */
    __gid_t gid;                 /* Owner's group ID.  */
    __uid_t cuid;         /* Creator's user ID.  */
    __gid_t cgid;         /* Creator's group ID.  */
    unsigned short int mode;/* Read/write permission.  */
    unsigned short int __pad1;
    unsigned short int __seq;/* Sequence number.  */
    unsigned short int __pad2;
    unsigned long int __unused1;
    unsigned long int __unused2;
};
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

10.2 消息队列的基本概念  

2011-12-19 21:56:00|  分类: 《Linux编程从入|字号 订阅

        消息队列是存储消息的线性表,是消息在传输过程中的容器。消息队列一经创建,即可向其写入指定类型的消息,其他进程则可以从该队列中取出指定类型的消息。

10.2.1 队列
        在数据结构中,队列定义为一种运算受限的线性表。它只允许在队列的一端进行插入,而在队列的另一端进行删除。队列的数据输入输出是按照“先进先出(FIFO)”的原则进行的。

10.2.2 消息
        消息就是进程间传递的数据的内容。在消息队列机制中,消息是有类型的。也就是说,一条消息包含的内容不仅是数据,而且包括消息的类型信息。对于同一个消息队列来说,可以存储各种不同类型的消息,而进程可以根据需要从消息队列中取出自己所需要的类型的消息。同时,也可以向队列中发送不同类型的消息。
        Linux的IPC机制为描述消息专门提供了一个数据结构模板,这个模板的名称是struct msgbuf,其定义位于<sys/msg.h>,具体定义如下所示。
struct msgbuf
{
    long int mtype;/* type of received/sent message */
    char mtext[1];/* text of the message */
};
参数说明:
1)mtype:消息类型。
2)mtext:消息内容,字符串数组。注意:此处的长度定义为1,由于该结构只是一个模板,在实际编程中,应根据该模板自行定义一个消息结构。这里的长度最大可以达到由系统内核规定的每条消息长度的最大值(kernel.msgmax)。

10.2.3 消息队列
        消息队列具有队列的基本特征,也具有其独特的性质。在消息队列中,入列称为发送消息,出列称为接收消息。除此之外,消息队列还具有以下特征。
1)消息队列中的消息是有类型的。在发送一条消息时,可以指定消息类型;在接收进程中,可以按照该消息类型从消息队列中获取数据。在实际应用中,可以多个应用共用一个队列,用消息类型区分不同的应用。
2)消息队列中的消息按照发送的顺序排队,对于相同类型的消息,先进入消息队列的消息先被接收。

提示:
        系统中对于消息队列等IPC机制是有极限值限制的。例如,在默认状态下,每条消息的最大字节数为8192字节;每个队列最多可容纳的字节数为16384字节;系统中允许最多建立16个消息队列等。这些参数可以通过执行命令 sysctl -a|grep msg得到。

10.2.4 消息队列数据结构
        消息队列创建后,操作系统在内核中分配了一个名称为 msqid_ds的数据结构用于管理该消息队列。在程序中可以通过函数msgctl对该结构进行读写,从而实现对该消息队列的控制功能。该结构的定义位于头文件 <bits/msq.h> 中,如下所示。
struct msqid_ds
{
  struct ipc_perm msg_perm;/* structure describing operation permission */
  __time_t msg_stime;/* time of last msgsnd command */
#if __WORDSIZE == 32
  unsigned long int __unused1;
#endif
  __time_t msg_rtime;/* time of last msgrcv command */
#if __WORDSIZE == 32
  unsigned long int __unused2;
#endif
  __time_t msg_ctime;/* time of last change */
#if __WORDSIZE == 32
  unsigned long int __unused3;
#endif
  unsigned long int __msg_cbytes; /* current number of bytes on queue */
  msgqnum_t msg_qnum;/* number of messages currently on queue */
  msglen_t msg_qbytes;/* max number of bytes allowed on queue */
  __pid_t msg_lspid;/* pid of last msgsnd() */
  __pid_t msg_lrpid;/* pid of last msgrcv() */
  unsigned long int __unused4;
  unsigned long int __unused5;
};
成员说明如下:
1)msg_perm:IPC许可权限结构。
2)msg_stime:最后一次向该消息队列发送消息(msgsnd)的时间。
3)msg_rtime:最后一次从该消息队列接收消息(msgrcv)的时间。
4)msg_ctime:最后一次调用msgctl的时间。
5)__msg_cbytes:当前该消息队列中的消息长度,以字节为单位。
6)msg_qnum:当前该消息队列中的消息条数。
7)msg_qbytes:该消息队列允许存储的最大长度,以字节为单位。
8)msg_lspid:最后一次向该消息队列发送消息(msgsnd)的进程ID。
9)msg_lrpid:最后一次从该消息队列接收消息(msgrcv)的进程ID。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

10.3 消息队列编程  

2012-02-04 15:13:36|  分类: 《Linux编程从入|字号 订阅

        System V的IPC机制提供了对消息队列进行编程的接口,主要包括消息队列的创建、消息的读写、消息队列的控制及删除。另外,IPC机制还提供了生成IPC键值的函数ftok。

10.3.1 键值生成函数
        键值生成函数ftok提供了一种键值生成方法,即根据文件名和一个整型的变量生成一个惟一的键值,并且保证每次以同样的参数调用ftok返回的键值是相同的。这里的整型变量成为项目ID。通过这种机制,使得编程人员不需要在程序中直接指定长整型的键值,而是通过定义一个文件和一个项目ID的方式即可。函数ftok的原型为:
#include <sys/ipc.h>
key_t ftok(const char *pathname,int prjid);
参数说明:
1)pathname:文件路径名称。可以使用绝对路径或者相对路径。
2)prjid:项目ID。指定相同的文件名称和一个不同的项目ID,可以生成不同的键值。
返回值:
1)若失败,返回-1。可能的原因包括文件不存在或者没有足够的访问权限。
2)若成功,返回其他数值。此时返回值即是可用的键值。
注意事项:
1)选择的文件应该是内容和属性(修改时间等)长期不发生变化的,并且是对进程而言是可读的,如“/etc/profile”等。如果文件内容或者属性经常发生变化,尽管用同样的参数去调用ftok,而返回的键值可能是不同的。
2)可以通过指定同一个文件,而使用不同的项目ID的方式获取不同的键值。

10.3.2 创建消息队列
        一个消息队列实际上是Linux在内核分配的一个数据结构。要使用消息队列进行消息传递,首先要创建一个消息队列。在Linux下,实现创建消息队列功能的是函数msgget。该函数除了具有创建新的消息队列的功能外,还可以返回一个已存在的消息队列的标识符。函数msgget的原型为:
#include <sys/msg.h>
int msgget(key_t key,int msgflg);
参数说明:
1)key:键值。该参数的输入方式有3种,分别是:一、直接指定一个整数;二、用ftok产生一个键值作为输入;三、用IPC_PRIVATE为参数输入,由系统创建一个可用的队列。
2)msgflg:标志和权限信息。通常情况下,在创建一个新的队列时,用参数IPC_CREAT;而在获取一个已存在的消息队列的标识符时,则将该参数设置为0。该参数包括两部分内容,一是标志,二是权限。
返回值:
若失败,返回-1;若成功,返回其他值,该值即是可用的标识符。
注意事项:
1)如果使用IPC_PRIVATE调用msgget,系统都将创建一个新的消息队列,即msgget调用将总是返回成功,并且该消息队列的键值恒为0。由此产生的问题是,在其他进程中不能用键值访问该消息队列,而必须用msgget返回的标识符来访问。所以,进程间必须通过某种机制(子进程继承等)来获取标识符,然后通过该标识符进行消息传递和接收。
2)msgget函数不仅有创建消息队列的功能,而且在消息队列已存在的情况下,还可以返回该消息队列的标识符。要特别注意IPC_CREAT和IPC_EXCL合用时的含义。

10.3.3 发送消息
        消息队列创建成功后,便可以调用消息发送函数向队列中发送消息了。消息的发送由函数msgsnd完成,其原型为:
#include <sys/msg.h>
int msgsnd(int msqid,const void *msgp,size_t msgsz,int msgflg);
参数说明:
1)msqid:消息队列的标识符,由msgget函数返回的标识符。
2)msgp:输入参数,消息结构指针,该参数的类型是void*,实际上应该向该参数传递一个“消息结构”的指针。不过在传递参数的时候强制类型转换为void*。
3)msgsz:消息的长度,这里的长度是指真正的消息内容的长度,而不是消息结构的尺寸。
4)msgflg:发送消息可选标志,如果不需要指定任何标志,在这里输入0即可。可以选择的标志只有IPC_NOWAIT。在默认状态下,消息的发送操作是阻塞的。一旦由于该消息队列中存储的消息已达到最大值或者存储的消息条数已达到最大值,此时调用msgsnd将阻塞,直到有进程调用msgrcv从队列中读出消息。如果选择了IPC_NOWAIT标志,表明消息发送的过程是非阻塞的。如果队列已满,则msgsnd将立即返回-1。
返回值:
若成功,返回0;若失败,返回-1。

10.3.4 接收消息
        消息已发送到消息队列中,其他进程如何获取该消息呢?这就需要用到消息接收的操作。利用消息接收操作可以从消息队列中读取指定类型的消息,也可以不指定类型,按照“先进先出”的原则读取队列头部的第一条消息。消息接收的函数是msgrcv,其原型为:
#include <sys/msg.h>
int msgrcv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg);
参数说明:
1)msqid:消息队列的标识符,由msgget函数返回的标识符。
2)msgp:输出参数,消息结构指针,该参数的类型是void*,实际上应该向该参数传递一个“消息结构”的指针。
3)msgsz:要读取的消息长度,在不确定消息长度的情况下,此处可以传递一个稍大的长度,系统将读取相应消息的全部内容,并在msgrcv的返回值中指明消息的正确长度。如果此处传递的要读取的长度比真正的消息内容长度要小,则默认状态下将返回失败。不过,如果指定了MSG_NOERROR可选标志,也可以成功读取。
4)msgflg:接收消息可选标志。
返回值:
若失败,返回-1;若成功,返回实际读取到的消息内容的字节数。

10.3.5 控制消息队列
        消息队列创建成功后,内核不仅创建了消息队列结构用于存储消息,并且创建了消息队列的控制结构(msqid_ds)。该结构中指定了该消息队列的权限信息、发送接收消息的进程等信息。IPC机制提供了专门的函数用于对该结构进行控制,这个函数就是msgctl,其原型为:
#include <sys/msg.h>
int msgctl(int msqid,int cmd,struct msqid_ds *buff);
参数说明:
1)msqid:消息队列的标识符,由msgget函数返回的标识符。
2)cmd:控制命令,可取值分别为:0(IPC_RMID)、1(IPC_SET)、2(IPC_STAT)、3(IPC_INFO)。
3)buff:输入或输出参数,指向msqid_ds结构的指针。在cmd参数为IPC_SET时,该参数用作输入;在cmd参数为IPC_STAT时,该参数用作输出。
返回值:
若成功,返回0;若失败,返回-1。

例10-1:编程实现消息队列的创建,消息的发送、接收及消息队列的控制。本例将编写4个程序(createmsq,sendmsg,recvmsg和ctrlmsq),分别用于创建消息队列、发送消息、接收消息和控制消息队列。
createmsq代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
void main()
{
//键值
key_t key;
//消息队列标识符
int msqid;
//生成键值
if((key=ftok("/etc/profile",1))==-1)
{
perror("ftok");
return;
}
//创建消息队列
if((msqid=msgget(key,IPC_CREAT|IPC_EXCL|0666))==-1)
{
//如果创建失败并且队列不存在
if(errno!=EEXIST)
{
perror("msgget");
return;
}
//如果创建失败但队列已经存在,则继续获取队列标识符
if((msqid=msgget(key,0))==-1)
{
perror("msgget");
return;
}
}
//创建成功,输出信息
printf("ok: msqid=%d\n",msqid);
}

sendmsg代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//自定义消息参数类型
typedef struct
{
long type;        //消息类型
char name[20];//消息内容
int id;//消息内容
}MSG;
void main(int argc,char *argv[])
{
//键值
key_t key;
//消息队列标识符
int msqid;
//消息变量
MSG msg;
//检测命令行参数
if(argc!=3)
{
printf("usage: sendmsg name id\n");
return;
}
//设置消息内容
memset(&msg,0,sizeof(MSG));
msg.type=1;
sscanf(argv[1],"%s",msg.name);
sscanf(argv[2],"%d",&msg.id);
//生成键值
if((key=ftok("/etc/profile",1))==-1)
{
perror("ftok");
return;
}
//获取消息队列标识符
if((msqid=msgget(key,0))==-1)
{
perror("msgget");
return;
}
//发送消息
if(msgsnd(msqid,&msg,sizeof(MSG)-4,0)<0) //阻塞模式发送
{
perror("msgsnd");
return;
}
//发送成功,输出信息
printf("ok: send message successful!\n");
}

recvmsg代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//自定义消息参数类型
typedef struct
{
long type;        //消息类型
char name[20];//消息内容
int id;//消息内容
}MSG;
void main()
{
//键值
key_t key;
//消息队列标识符
int msqid;
//消息变量
MSG msg;
//生成键值
if((key=ftok("/etc/profile",1))==-1)
{
perror("ftok");
return;
}
//获取消息队列标识符
if((msqid=msgget(key,0))==-1)
{
perror("msgget");
return;
}
//接收消息
memset(&msg,0,sizeof(MSG));
if(msgrcv(msqid,&msg,sizeof(MSG)-4,1,0)<0)
{
perror("msgrcv");
return;
}
//接收消息成功,输出信息
printf("ok: recveive message: name=%s,id=%d\n",msg.name,msg.id);
}

ctrlmsg代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//自定义消息参数类型
typedef struct
{
long type;//消息类型
char name[20];//消息内容
int id;//消息内容
}MSG;
void main()
{
//键值
key_t key;
//消息队列标识符
int msqid;
//消息变量
MSG msg;
//消息队列控制结构变量
struct msqid_ds msqds;
//生成键值
if((key=ftok("/etc/profile",1))==-1)
{
perror("ftok");
return;
}
//获取消息队列标识符
if((msqid=msgget(key,0))==-1)
{
perror("msgget");
return;
}
//获取消息队列状态
memset(&msqds,0,sizeof(msqds));
if(msgctl(msqid,IPC_STAT,&msqds)<0)
{
perror("msgctl IPC_STAT");
return;
}
//获取消息队列状态成功,输出相关信息
printf("max msq size=%d bytes\n",msqds.msg_qbytes);//输出该消息队列允许存储的最大长度
printf("current msg count=%d\n",msqds.msg_qnum);//输出当前该队列的消息数目
printf("current msg size=%d bytes\n",msqds.__msg_cbytes); //输出当前该队列中的消息总长度
}

说明:
1)编译链接分别生成可执行程序:createmsq,sendmsg,recvmsg及ctrlmsq。
2)创建消息队列,执行命令: ./createmsq
3)发送消息,执行命令: ./sendmsg Jim 123
4)接收消息,执行命令: ./recvmsg
5)获取消息队列状态信息,执行命令: ./ctrlmsq
6)注意观察控制台的输出结果。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

11. 进程间通信之共享内存  

2012-02-04 15:25:37|  分类: 《Linux编程从入|字号 订阅

导读:由于本章篇幅较长,所以把它拆分为如下几节。
11.1 共享内存的基本概念
11.2 共享内存编程
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

11.1 共享内存的基本概念  

2012-02-04 16:35:07|  分类: 《Linux编程从入|字号 订阅

        共享内存是IPC机制提供的另一种进程间通信机制。消息队列在实现消息的收发时,首先由发送进程从进程空间将数据复制至内核分配的数据缓冲区,接收进程再从内核的缓冲区复制到进程的虚拟地址空间。与消息队列的机制不同,共享内存是通过将内核分配的共享存储区映射到进程的地址空间实现的,没有数据的复制过程。
        共享内存是内核分配的一块存储区,多个进程通过共享内存进行通信是指,同一块物理内存被映射到多个进程各自的地址空间,各个进程都可以对共享内存中的数据进行读写操作。

11.1.1 共享内存的编程模型
        要使用共享内存,首先应该创建一个共享内存。在Linux系统中,这是由函数shmget实现的。共享内存创建后,需要调用shmat将该共享内存映射到调用进程的地址空间。映射完成后,通过返回的指针对共享内存进行读写。当进程不需要再对共享内存操作时,调用shmdt关闭共享内存的映射。最后,在系统不再需要该共享内存时,调用shmctl将共享内存删除,释放系统内存。共享内存编程模型如下图11-1所示。
11.1 共享内存的基本概念 - Jim - Jim Liang
 图11-1:共享内存编程模型

11.1.2 共享内存数据结构
        与其他IPC机制相同,共享内存也有键值与标识符。键值的生成使用与消息队列相同。与消息队列不同的是,共享内存有访问计数器机制。每一个进程进行一次共享内存映射,共享内存计数器就会加1;而每做一次解除映射操作,该计数器就会减1。只有共享内存的访问计数器为0时,才能真正地删除一块共享内存。可以通过执行命令  ipcs -m 的输出项nattch查看共享内存的访问计数。
        共享内存创建后,系统内核也为该共享内存分配了控制结构,该结构名称为 struct shmid_ds,这与消息队列是相似的。在程序中可以通过函数shmctl对该结构进行读写,从而实现对该共享内存的控制功能。该结构的声明位于头文件 <bits/shm.h> 中,如下所示。
struct shmid_ds
{
    struct ipc_perm shm_perm;/* operation permission struct */
    size_t shm_segsz;/* size of segment in bytes */
    __time_t shm_atime;/* time of last shmat() */
#if __WORDSIZE == 32
    unsigned long int __unused1;
#endif
    __time_t shm_dtime;/* time of last shmdt() */
#if __WORDSIZE == 32
    unsigned long int __unused2;
#endif
    __time_t shm_ctime;/* time of last change by shmctl() */
#if __WORDSIZE == 32
    unsigned long int __unused3;
#endif
    __pid_t shm_cpid;/* pid of creator */
    __pid_t shm_lpid;/* pid of last shmop */
    shmatt_t shm_nattch;        /* number of current attaches */
    unsigned long int __unused4;
    unsigned long int __unused5;
};
成员说明如下:
1)shm_perm:IPC许可权限结构。
2)shm_segsz:共享内存的大小,以字节为单位。
3)shm_atime:最后一次调用shmat的时间。
4)shm_dtime:最后一次调用shmdt的时间。
5)shm_ctime:最后一次调用shmctl修改共享内存属性的时间。
6)shm_cpid:创建共享内存的进程ID。
7)shm_lpid:最后一次操作共享内存的进程ID。
8)shm_nattch:当前共享内存的映射计数器。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

11.2 共享内存编程  

2012-02-05 15:21:54|  分类: 《Linux编程从入|字号 订阅

11.2.1 创建共享内存
        要使用共享内存,首先应该创建共享内存。在Linux下,是通过函数shmget实现的。shmget的原型为:
#include <sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
参数说明:
1)key:键值。其生成方式见《10.3 消息队列编程》中对函数msgget的介绍。
2)size:要创建的共享内存的大小,以字节为单位。
3)shmflg: 标志和权限信息。通常情况下,在创建一个新的共享内存时,用参数IPC_CREAT;而在获取一个已存在的共享内存的标识符时,则将该参数设置为0。该参数包括两部分内容,一是标志,二是权限。
返回值:
若出错,返回-1;若成功,返回共享内存的描述符。
注意事项:
1)共享内存与消息队列一样,内核中对其大小也有限制。可以通过执行 sysctl -a|grep shm 命令查看系统内核对大小的限制,如果有必要,可以通过修改 /etc/sysctl.conf 修改内核配置。

11.2.2 映射共享内存
        共享内存创建成功后,在整个操作系统内是全局可见的。只要具备访问权限,应用程序都可以使用该共享内存。在使用共享内存时,要将共享内存映射到本地进程的地址空间。函数shmat可以实现这一映射功能。其原型为:
#include <sys/shm.h>
void *shmat(int shmid,const void *shmaddr,int shmflg);
 参数说明:
1)shmid:共享内存标识符,有shmget返回。
2)shmaddr:该参数是由调用shmat的进程指定的一个虚拟空间地址,新创建的共享内存将附在该地址后。通常情况下,直接传递NULL给该参数即可。
3)shmflg: 标志和权限信息。
返回值:
若出错,返回-1;若成功,返回映射到本地进程的共享内存地址。

11.2.3 解除共享内存映射
        在程序对共享内存访问结束后,应调用shmdt解除共享内存与进程地址空间的映射。shmdt的原型为:
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数说明:
1)shmaddr:共享内存映射至本地的地址,即由shmat返回的地址。
返回值:
若成功,返回0;若失败,返回-1。
注意事项:
1)当进程不再使用共享内存后,要记住调用shmdt解除共享内存的映射。虽然进程在退出时系统会自动删除其与共享内存的映射,但是显式调用shmdt是一种良好的编程风格。
2)共享内存有引用计数器。调用shmdt只是将该共享内存的引用计数器减1,只有引用计数值变为0的情况下,才可以调用shmctl安全地将共享内存从系统中删除。

11.2.4 控制共享内存
        与消息队列一样,共享内存创建后,系统在内核为其分配了一个类型为 struct shmid_ds 的控制结构。IPC提供了函数获取该结构并通过修改该结构控制共享内存,这个函数就是shmctl,其原型为:
#include <sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds *buff);
参数说明:
1)shmid: 共享内存标识符,有shmget返回。
2)cmd: 控制命令,可取值分别为:0(IPC_RMID)、1(IPC_SET)、2(IPC_STAT)、3(IPC_INFO)。
3)buff:输入或输出参数,指向shmid_ds结构的指针。 在cmd参数为IPC_SET时,该参数用作输入;在cmd参数为IPC_STAT时,该参数用作输出。
返回值:
若成功,返回0;若失败,返回-1。

例11-1:编写两个程序writeshm和readshm,其中writeshm实现创建共享内存并向共享内存中写入数据,而readshm则从共享内存中读取数据并删除共享内存。
writeshm代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
//自定义共享内存数据类型
typedef struct
{
char name[26];
int id;
}SHM;
void main()
{
//键值
key_t key;
//共享内存标识符
int shmid;
//自定义数据结构指针
SHM *pShm;
//生成键值
if((key=ftok("/etc/profile",1))==-1)
{
perror("ftok");
return;
}
//创建共享内存
if((shmid=shmget(key,sizeof(SHM),IPC_CREAT|0666))==-1)
{
perror("shmget");
return;
}
//输出共享内存标识符
printf("shmid=%d\n",shmid);
//映射共享内存到本地
if((pShm=shmat(shmid,NULL,0))==(void*)-1)
{
perror("shmat");
goto ErrorExit;
}
//向共享内存写入数据
strcpy(pShm->name,"Jim");
pShm->id=123;
//解除共享内存映射
if(shmdt(pShm)==-1)
perror("shmdt");
return;
ErrorExit:
//若发生错误则删除共享内存
if(shmctl(shmid,IPC_RMID,NULL)<0)
perror("shmctl IPC_RMID");
}

readshm代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
//自定义共享内存数据类型
typedef struct
{
char name[26];
int id;
}SHM;
void main()
{
//键值
key_t key;
//共享内存标识符
int shmid;
//自定义数据结构指针
SHM *pShm;
//生成键值
if((key=ftok("/etc/profile",1))==-1)
{
perror("ftok");
return;
}
//获取共享内存
if((shmid=shmget(key,sizeof(SHM),0))==-1)
{
perror("shmget");
return;
}
//输出共享内存标识符
printf("shmid=%d\n",shmid);
//映射共享内存到本地
if((pShm=shmat(shmid,NULL,0))==(void*)-1)
{
perror("shmat");
return;
}
//输出共享内存中的数据
printf("name=%s\n",pShm->name);
printf("id=%d\n",pShm->id);
//解除共享内存映射
if(shmdt(pShm)==-1)
perror("shmdt");
//删除共享内存
if(shmctl(shmid,IPC_RMID,NULL)<0)
perror("shmctl IPC_RMID");
}

说明:
1)编译链接分别生成可执行程序:writeshm和readshm。
2)创建共享内存并写入数据,执行命令: ./writeshm
3)读取共享内存并将其删除,执行命令: ./readshm
4)注意观察控制台的输出结果。

提示 :在Linux系统中,共享内存物理上是位于shm文件系统中的文件。该文件系统挂载于 /dev/shm 设备上,文件系统名称为tmpfs。tmpfs文件系统是完全驻留在系统内存中的,其访问速度非常快。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

12. 进程间通信之信号量  

2012-02-05 15:31:33|  分类: 《Linux编程从入|字号 订阅

导读:由于本章篇幅较长,所以把它拆分为如下几节。
12.1 PV操作原理
12.2 信号量的基本概念
12.3 信号量编程
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

12.1 PV操作原理  

2012-02-12 13:55:39|  分类: 《Linux编程从入|字号 订阅

        PV操作是信号量实现的基础。

12.1.1 PV操作的定义
        PV操作是两个操作的合称,这两个操作分别成为P操作和V操作。P的中文意思是“通过”,V的中文意思是“释放”。
        P操作和V操作都是一种操作系统“原语”。原语是指不可中断的过程。可以这样理解为P和V操作都是原子操作,由操作系统来保证P操作和V操作的不可中断性。PV操作的前提是假定存在整型变量sem,PV操作的过程就是对该变量进行加减的过程。

12.1.1.1 P操作
        P操作的过程定义如下:
1)sem减1。
2)若sem>=0,则P操作返回,该进程可以“通过”并继续执行。
3)若sem<0,则该进程被阻塞,进入操作系统的阻塞队列。

12.1.1.2 V操作
        V操作的过程定义如下:
1)sem加1(释放)。
2)若sem>0,则V操作返回,该进程继续执行。
3)若sem<=0,则从操作系统的阻塞队列中唤醒一个阻塞在该信号量上的进程,然后再返回原进程(调用V操作的进程)继续执行。

12.1.2 PV操作的应用
        PV操作在进程间通信过程中应用广泛,不仅可以用来实现进程间对资源的互斥访问,而且可以实现进程间的同步。

12.1.2.1 进程互斥
        假设系统中存在一个文件,两个进程A和B都要对其进行写操作,如果进程A或者B单独对文件进行写操作都不会存在问题。但是,一旦出现进程A和B同时对文件进行写,那么不可避免地会导致文件内容的混乱。可以使用PV操作解决该问题。首先定义一个信号量,其初始值假定为1。在进程A向文件写入之前,首先调用P操作原语锁定资源,此时资源是可用的,P操作成功返回。进程A打开文件并向其中写入数据,写入数据完成后,关闭文件并调用V操作原语释放资源。进程B的操作过程与A相同。这个过程用伪代码描述如下:
P(sem);
WriteFile;
V(sem);

12.1.2.2 进程同步
        在多进程编程环境下,有时一个进程(进程A)需要暂时阻塞,等待另外的进程(进程B)进行相应的处理。进程B处理结束后,进程A再继续执行。以经典的计算——打印问题为例,假设其应用系统有两个进程A和B,进程A负责进行问题的计算,进程B负责结果的打印。两个进程同时开始运行,进程A计算问题结果需要一定时间,而进程B必须要等待A执行完成后方可继续。可以用PV操作来解决:在系统中定义一信号量,其初始值为0。进程A执行计算任务,执行完成后调用V操作通知进程B打印。而进程B一开始就执行P操作,等待进程A计算结束。这个过程用伪代码描述如下:
进程A:ComputeResult-->V(sem)
进程B:P(sem)-->PrintResult
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

12.2 信号量的基本概念  

2012-02-12 15:52:49|  分类: 《Linux编程从入|字号 订阅

12.2.1 Linux信号量简介
        与消息队列及共享内存不同,信号量实现的是一种类似计数器的功能,而不是用于存储进程间通信数据。这种机制由操作系统的PV操作发展而来,主要用于实现进程间的互斥及同步。
        Linux下的信号量机制在标准的PV操作基础上进行了扩充,主要体现在以下几点:
1)Linux支持信号量组。一次可以同时创建一组信号量,并可对组内的信号量进行不同操作。
2)每次信号量的PV操作不仅限于对信号量值加一或者减一,而且可以加减任意整数。
3)支持进程异常退出时回滚对信号量的修改。

12.2.2 信号量的控制结构
        与消息队列和共享内存相同,信号量也有一个控制结构。这个控制结构在创建信号量成功后,由内核产生。其结构声明位于头文件 <bits/sem.h> 中,如下所示:
struct semid_ds
{
  struct ipc_perm sem_perm;/* operation permission struct */
  __time_t sem_otime;/* last semop() time */
  unsigned long int __unused1;
  __time_t sem_ctime;/* last time changed by semctl() */
  unsigned long int __unused2;
  unsigned long int sem_nsems;/* number of semaphores in set */
  unsigned long int __unused3;
  unsigned long int __unused4;
};
成员说明:
1)sem_perm:IPC许可权限结构。
2)sem_otime:最后一次调用semop操作的时间。
3)sem_ctime:最后一次调用semctl改变信号量的时间。
4)sem_nsems:信号量集中的信号量个数。Linux系统可以对一个信号量集进行操作与控制。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

12.3 信号量编程  

2012-02-26 14:28:37|  分类: 《Linux编程从入|字号 订阅

12.3.1 创建信号量
        要使用信号量,首先需要创建信号量。Linux提供了系统调用semget用于创建信号量,其原型为:
#include <sys/sem.h>
int semget(key_t key,int nsems,int semflg);
参数说明:
1)key: 键值。其生成方式见《10.3 消息队列编程》中对函数msgget的介绍。
2)nsems:信号量集中信号量的个数。
3)semflg:创建信号量的标志。关于创建标志的详细说明,请参见 《10.3 消息队列编程》中对函数msgget的介绍。
返回值:
若失败,返回-1;若成功,返回信号量的描述符。
注意事项:
1)semget创建的信号量,实际上是一个信号量集。semget提供了一次性创建n个信号量的功能,其信号量编号为0~n-1。
2)semget不仅用于生成新的信号量集,而且用于获取已存在的信号量集标识符。
3)在信号量已存在的情况下,如果调用semget只指定了IPC_CREAT标志,那么参数nsems必须与创建时指定的值一致,否则semget将返回失败;如果调用semget时指定了IPC_CREAT|IPC_EXCL标志,semget将返回EEXIST错误,即使是nsems与创建时不同也是如此。

12.3.2 信号量操作
        对资源的互斥访问及进程间同步是通过信号量操作完成的。信号量操作其实就是P和V操作。在信号量创建成功后,就可以通过信号量操作实现进程的互斥及同步了。Linux提供了系统调用semop对信号量进行操作,其原型为:
#include <sys/sem.h>
int semop(int semid,struct sembuf *sops,size_t nsops);
参数说明:
1)semid:信号量集标识符,该标识符由semget返回。
2)sops:信号量集的操作缓存,该参数是一个指向struct sembuf结构数组的指针。该结构数组的每一项定义了一个信号量的操作数据。该结构定义见下面“资料补充1”。
3)nsops:操作的信号量个数。每次semop调用可以对一个信号量集中的多个信号量进行操作。
返回值:
若成功,返回0;若失败,返回-1。

资料补充1:
struct sembuf
{
  unsigned short int sem_num;   //信号量编号,同一个信号量集中各个信号量的编号为:0~n-1
  short int sem_op;           //信号量的操作数
  short int sem_flg;           //信号量的操作标志
};
信号量的操作数可以分为若干情况,具体如下:
1)sem_op>0:表示要释放由该信号量控制的资源,将信号量的值增加sem_op。
2)sem_op=0:表示要等待到信号量的值变为0。如果信号量的值为0,则立即返回;否则进入阻塞,直到信号量值为0。
3)sem_op<0:表示要获取由该信号量控制的资源。如果当前信号量的值减去|sem_op|大于等于0,则将信号量的值减去|sem_op|;如果当前信号量的值减去|sem_op|小于0,则信号量的值不变,调用进程进入阻塞状态,直至信号量的值大于等于|sem_op|。此时,被阻塞的调用进程将激活,并将信号量的值减去|sem_op|。
信号量的操作标志主要包括IPC_NOWAIT和SEM_UNDO,具体如下:
1)IPC_NOWAIT:如果设置了该标志,则在sem_op中应该阻塞的调用都将立即返回失败。错误码errno为EAGAIN。
2)SEM_UNDO:如果设置了该标志,内核将分配一个sem_undo的数据结构记录对该信号量的操作。在进程结束时,该进程对该信号量进行的semop操作将被撤销。设置该标志的意义在于,一旦进程没有释放信号量就退出(异常终止),系统内核将根据sem_undo的记录自动代为释放。

例12-1:编写两个程序semtest1和semtest2,其中semtest1创建并初始化信号量,在初始化完成后,占用信号量10秒的时间;而semtest2启动后获取信号量,并尝试占用信号量。
semtest1代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

//自定义数据类型,该数据类型是进行semctl操作时所需要的
typedef union semun
{
 int val;
 struct semid_ds *semds;
 unsigned short *array;
 struct seminfo *info;
}SEM_CTL_UN;

/*子程序:PV操作的封装
参数pv的取值为:'p','v','w'
其中:'p'代表p操作;'v'代表v操作;'w'代表等待信号量
 */
int pv_sem(int semid,char pv)
{
struct sembuf buff={0};

buff.sem_num=0;
if(pv=='p')
buff.sem_op=-1;
else if(pv=='v')
buff.sem_op=1;
else if(pv=='w')
buff.sem_op=0;
else return -1;
buff.sem_flg=SEM_UNDO;

if(semop(semid,&buff,1)==-1)return -1;

return 0;
}

//主程序
void main()
{
key_t key;
int semid,i;
SEM_CTL_UN semctlarg;

//生成键值
key=ftok("/etc/profile",1);
if(key<0)
{
perror("ftok");
return;
}

//创建信号量
semid=semget(key,1,IPC_CREAT|0666);
if(semid<0)
{
perror("semget");
return;
}

//初始化信号量
semctlarg.val=1;
if(semctl(semid,0,SETVAL,semctlarg)<0)
{
perror("semctl");
return;
}

//执行P操作
if(pv_sem(semid,'p')<0)
{
perror("pv_sem of p");
return;
}

//模拟在临界区操作
for(i=0;i<10;i++)
{
printf("semtest1 do critical things: %d%%\n",i*10);
sleep(1);
}
printf("semtest1 do critical things: %d%%\n",i*10);

//执行V操作
if(pv_sem(semid,'v')<0)
{
perror("pv_sem of v");
return;
}
}

semtest2代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/*子程序:PV操作的封装
参数pv的取值为:'p','v','w'
其中:'p'代表p操作;'v'代表v操作;'w'代表等待信号量
*/
int pv_sem(int semid,char pv)
{
struct sembuf buff={0};

buff.sem_num=0;
if(pv=='p')
buff.sem_op=-1;
else if(pv=='v')
buff.sem_op=1;
else if(pv=='w')
buff.sem_op=0;
else return -1;
buff.sem_flg=SEM_UNDO;

if(semop(semid,&buff,1)==-1)return -1;

return 0;
}

//主程序
void main()
{
key_t key;
int semid,i;

//生成键值
key=ftok("/etc/profile",1);
if(key<0)
{
perror("ftok");
return;
}

//获取信号量
semid=semget(key,1,0);
if(semid<0)
{
perror("semget");
return;
}

//执行P操作
if(pv_sem(semid,'p')<0)
{
perror("pv_sem of p");
return;
}

//模拟在临界区操作
for(i=0;i<10;i++)
{
printf("semtest2 do critical things: %d%%\n",i*10);
sleep(1);
}
printf("semtest2 do critical things: %d%%\n",i*10);

//执行V操作
if(pv_sem(semid,'v')<0)
{
perror("pv_sem of v");
return;
}
}

说明:
1)编译链接分别生成可执行程序:semtest1和semtest2
2)打开两个命令台,分别cd进入到semtest1和semtest2所在的目录
3)在命令台1中执行命令: ./semtest1
4)紧接着马上在命令台2中执行命令: ./semtest2
5)注意观察两个控制台的输出结果。
6)可以在测试过程中按[ctrl]+[c]组合键终止semtest1的执行,观察SEM_UNDO标志的作用。

12.3.3 信号量控制
        在进行信号量操作时,经常需要对信号量进行控制,如设置信号量的值、获取信号量的值等。Linux提供了系统调用semctl完成对信号量的控制。其原型为:
#include <sys/sem.h>
int semctl(int semid,int semnum,int cmd, ... );
参数说明:
1)semid: 信号量集标识符,该标识符由semget返回。
2)semnum:信号量的编号。
3)cmd:信号量控制命令,具体见“资料补充2”。
4)...:控制命令的参数,对于不同的控制命令,其参数格式是不同的,为满足这一需要,该参数使用了共用体,具体参见“资料补充3”。
返回值:
若失败,返回-1;若成功,返回其他值。

资料补充2:
系统支持的信号量控制命令如下:
1)IPC_STAT:读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
2)IPC_SET:设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
3)IPC_RMID:将信号量集从内存中删除。
4)GETALL:用于读取信号量集中的所有信号量的值。
5)GETNCNT:返回正在等待资源的进程数目。
6)GETPID:返回最后一个执行semop操作的进程的PID。
7)GETVAL:返回信号量集中的一个单个的信号量的值。
8)GETZCNT:返回这在等待完全空闲的资源的进程数目。
9)SETALL:设置信号量集中的所有的信号量的值。
10)SETVAL:设置信号量集中的一个单独的信号量的值。

资料补充3:
union semun
{
int val;
struct semid_ds *semds;
unsigned short *array;
struct seminfo *info;
};
成员说明:
1)val:用于SETVAL命令,设置信号量的值。
2)semds:用于IPC_STAT和IPC_SET命令,指向semid_ds结构,用于获取或设置信号量控制结构。
3)array:用于GETALL和SETALL命令,获取或者设置信号量集的值。
4)info:用于IPC_INFO命令,该命令是Linux系统下特有的,用于返回系统内核定义的信号量极值的定义信息。该成员为一个结构指针,结构类型struct seminfo的定义如下:
struct  seminfo
{
  int semmap;   //信号量映射入口,暂未用
  int semmni;   //系统中允许创建的信号量集的最大数目
  int semmns;   //系统中允许创建的信号量的最大数目
  int semmnu;   //暂未用
  int semmsl;   //一个信号量集中允许包含的最大信号量数目
  int semopm;   //一次semop可以同时操作的最大信号量数目
  int semume;   //每个进程允许sem_undo结构的最大数目,暂未用
  int semusz;   //sem_undo结构的大小
  int semvmx;   //信号量允许设置的最大值
  int semaem;   //信号量的sem_undo机制可以记录的最大值
};

例12-2:创建一个包含两个信号量的信号量集,用SETALL命令设置其初始值,然后用GETALL命令获取信号量的值。
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

//自定义数据类型,该数据类型是进行semctl操作时所需要的
typedef union semun
{
 int val;
 struct semid_ds *semds;
 unsigned short *array;
 struct seminfo *info;
}SEM_CTL_UN;

//主程序
void main()
{
key_t key;
int semid,i;
ushort InArr[2],OutArr[2]={0};
SEM_CTL_UN semctlarg;

//生成键值
key=ftok("/etc/profile",0);
if(key<0)
{
perror("ftok");
return;
}

//创建包含两个信号量的信号量集
semid=semget(key,2,IPC_CREAT|0666);
if(semid<0)
{
perror("semget");
return;
}

//初始化信号量集中各个信号量的值
InArr[0]=2;
InArr[1]=3;
semctlarg.array=InArr;
if(semctl(semid,2,SETALL,semctlarg)<0)
{
perror("semctl of SETALL");
return;
}

//读取信号量集中各个信号量的值
semctlarg.array=OutArr;
if(semctl(semid,2,GETALL,semctlarg)<0)
{
perror("semctl of SETALL");
return;
}

//打印信号量的值
for(i=0;i<2;i++)
printf("semphore %d's value is %d\n",i,OutArr[i]);
}

说明:经过编译链接,执行该程序的结果如下所示:
semphore 0's value is 2
semphore 1's value is 3
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

13. 基本套接口编程  

2012-02-26 15:05:49|  分类: 《Linux编程从入|字号 订阅

导读:由于本章篇幅较长,所以把它拆分为如下几节。
13.1 套接口编程简述
13.2 套接口数据结构
13.3 基本套接口函数
13.4 套接口选项
13.5 TCP套接口编程
13.6 UDP套接口编程
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

13.1 套接口编程简述  

2012-02-27 22:34:03|  分类: 《Linux编程从入|字号 订阅

        套接口是Linux系统下主要的网络编程接口。套接口最初是在BSD版本的UNIX上实现的,随着采用BSD UNIX计算机厂商的日益增多,套接口被广泛认可并逐渐成为事实上的工业标准。目前,几乎所有的操作系统都提供了对套接口的支持。
        套接口是位于应用层与TCP/IP协议族通信的中间软件抽象层,它逻辑上位于传输层与应用层之间,实际上由一组网络编程API组成。套接口的英文名称是Socket,中文名称为插口或者套接字。Socket数据传输是一种特殊的I/O,与文件操作相对应,Socket也可以看作是一种文件描述符。

13.1.1 半相关与全相关
        在网络通信模型中,一个连接一旦建立,必然包括以下要素:协议、本地地址、本地端口号、远程地址、远程端口号。这样的一组要素,称其为“五元组”或者“全相关”。这里的协议是指通信协议,如TCP协议或者UDP协议等。本地地址及本地端口号定义网络连接的本地参数,而远程地址及远程端口号定义了通信对端的连接参数。
        在上述五元组中,协议、本地地址和本地端口号这三项要素惟一地标识了网络连接的本地进程,而协议、远程地址和远程端口号则惟一地标识了网络连接的对端进程。这三项要素又称为“三元组”。由于三元组指定了一个完整的网络连接的半部分,所以称为“半相关”。

13.1.2 协议族与地址族
        Linux套接口支持多种协议族。协议族称为域,不同的协议族定义了不同的通信环境。Linux支持的协议族及协议族的定义包含在头文件 <bits/socket.h> 中,本章介绍的网络编程使用 PF_INET 协议族。
        根据套接口规范的定义,一个协议族可以支持多个地址族,但是,到目前为止的套接口实现中,全部都是一个协议族支持一个地址族。因此,在各个版本的Linux系统中,都把协议族与地址族定义为同一个值。在实际的编程过程中,考虑到将来的可移植性,在调用创建套接口的函数时,应该使用PF系列宏定义;而在类似地址绑定的系统调用中,应该使用AF系列宏定义。

13.1.3 面向连接与面向无连接
        在套接口编程模型中,存在面向连接的服务与面向无连接的服务。在面向连接的服务模式下,每一次完整的数据传输都要经过建立连接、使用连接、终止连接的过程。而在面向无连接的服务模式下,只需要知道通信对端的信息,即可直接向目标发送数据。

13.1.4 套接口类型
        在创建套接口时,除需要指定协议族外,还需要指定套接口的类型。Linux支持多种套接口类型,主要包括如下三种:
1)流式套接口:(SOCK_STREAM):提供面向连接、可靠的全双工数据传输服务。流式套接口通过TCP协议实现。
2)数据报式套接口(SOCK_DGRAM):提供无连接服务。数据报套接口通过UDP协议实现。
3)原始套接口(SOCK_RAW):该套接口允许对较低层协议(如IP、ICMP等)进行直接访问。在某些应用中,使用原始套接口可以构建自定义头部信息的IP报文。创建原始套接口需要超级用户权限。

13.1.5 字节序
        字节序是指占内存大于一个字节的类型的数据在内存中的存储顺序,按照不同的顺序可以划分为小端字节序(Little endian)、大端字节序(Big endian)两种,统称为主机字节序。
        小端字节序指低字节数据存放在内存低地址处,高字节存放在内存高地址处;大端字节序指高字节数据存放在低地址处,低字节数据存放在高地址处。
        在TCP/IP协议中,规定统一采用大端字节序。

例13-1:编程查看当前系统的字节序。
代码如下:
#include <stdio.h>
#include <stdlib.h>

//函数:获取系统大小端
//返回1表示小端,返回0表示大端。
int IsLittleEndian()
{
unsigned short i=1;
return (*((char*)(&i))==1);
}

void main()
{
if(IsLittleEndian())
printf("Local system is Little Endian\n");
else
printf("Local system is Big Endian\n");
}

13.1.6 套接口连接方式
        在面向连接的套接口编程模式下,可以根据应用的需要构建不同的连接方式。主要的连接方式包括短连接和长连接。短连接是指在每一次传输数据完毕后立即断开连接,而长连接是指可以多次收发数据的连接,直到不再需要数据传输时才关闭连接。实际使用哪种连接模式视应用程序具体需要而定。

13.1.7 数据传输方式
        连接建立后,在数据收发过程中也存在不同的方式,主要包括同步和异步两种方式。这里的同步和异步是从应用程序的逻辑结构上理解的。工作方式如下:
1)同步传输:发送方发送报文后,等待接收方的应答再发送下一个包,若接收方不能在规定的时间内返回应答报文,则执行超时处理。
2)异步传输:发送方的发送进程只负责发送数据,不需要等待接收方返回任何数据;同样,接收方的接收进程只负责接收数据,不需要向发送方回答任何数据。通常情况下,异步方式在客户端和服务器端各有两个进程专门负责数据收发。
        在实际网络编程过程中,建立网络通信模型需要综合考虑连接方式及数据传输方式。比较有价值的网络通信模型包括同步短连接、同步长连接、异步长连接等。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

13.2 套接口数据结构  

2012-03-31 15:48:28|  分类: 《Linux编程从入|字号 订阅

        在套接口编程接口函数中,定义了若干数据结构,这些数据结构大多为结构体类型,基本上所有的套接口函数都会用到这些结构的内容。

13.2.1 套接口地址结构
        在套接口编程模式下,大多接口函数都需要用到套接口地址结构。套接口地址结构就是包含了套接口的地址及端口等信息的结构体。该结构体在调用大多数套接口函数时是必需的。Linux系统支持多种地址族的套接口,而每种套接口又有其特有的地址结构。在TCP/IP协议下进行网络编程使用的地址族是Internet地址族,即INET。Internet地址族的地址结构为sockaddr_in,其中的in代表internet地址族。该结构的定义位于 /usr/include/netinet/in.h,其声明如下:
/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;/* Port number.  */
    struct in_addr sin_addr;/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
   __SOCKADDR_COMMON_SIZE -
   sizeof (in_port_t) -
   sizeof (struct in_addr)];
};
成员说明:
1)__SOCKADDR_COMMON (sin_):该成员是一个宏定义,指定地址族,等价于 sa_family_t sin_family。
2)sin_port:端口号,必须为网络字节序。
3)sin_addr:IP地址,必须为网络字节序。该成员是一个结构变量,结构中惟一的成员是IP地址,数据类型为uint32_t。
4)sin_zero:为与通用套接口地址结构保持大小一致而填充的数据。在调用套接口编程时,往往需要将地址结构进行强行类型转换,转换为通用套接口地址结构进行传递参数。为满足这一要求,需要保持两个数据结构大小一致。该成员内容应该设置为0。

13.2.2 通用套接口地址结构
        Linux支持多种不同的地址族,每种地址族的结构内容是各不相同的。在向套接口的编程接口函数传递地址结构指针时,需要将各不相同的地址结构转换为一个通用的数据结构,这就是通用套接口地址结构。在<bits/socket.h>中定义,声明如下:
/* Structure describing a generic socket address.  */
struct sockaddr
{
    __SOCKADDR_COMMON (sa_);/* Common data: address family and length.  */
    char sa_data[14];/* Address data.  */
};
成员说明:
1)__SOCKADDR_COMMON(sa_):该成员是一个宏定义,指定地址族,等价于 sa_family_t sa_family。
2)sa_data:地址数据。对于Internet地址族来说,就是包括sin_port、sin_addr、sin_zero在内的全部地址数据。

13.2.3 主机名称数据结构
        在进行网络编程时,通常需要用到主机名称(域名)与IP地址转换。在这种情况下,就需要使用主机名称数据结构hostent,该数据结构定义了主机名与IP地址的对应关系。在套接口编程模型中,与地址绑定相关的操作都需要使用该结构。该结构定义在头文件netdb.h中,其声明如下:
/* Description of data base entry for a single host.  */
struct hostent
{
  char *h_name; /* Official name of host.  */
  char **h_aliases;/* Alias list.  */
  int h_addrtype; /* Host address type.  */
  int h_length; /* Length of address.  */
  char **h_addr_list;/* List of addresses from name server.  */
#if defined __USE_MISC || defined __USE_GNU
# define h_addrh_addr_list[0] /* Address, for backward compatibility.*/
#endif
};
成员说明:
1)h_name:主机名称。
2)h_aliases:主机别名列表。主机别名可能存在多个,该成员为指向别名列表的指针。
3)h_addrtype:主机地址类型。在Internet地址族下,该值一般为AF_INET。
4)h_length:地址的字节长度。
5)h_addr_list:一个以0结尾的数组,包含该主机的所有地址。
6)h_addr:在h_addr_list中的第一个地址。
        用于获取该结构的接口函数有:gethostbyname、gethostbyaddr及gethostend等。其中,gethostbyname用于把主机名映射成IP地址,而gethostbyaddr的作用则相反。

例13-2:查看当前系统的主机名称及IP地址。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netdb.h>
#include <error.h>

void main()
{
int i;
struct hostent *h;
char **p; //指向字符串的指针
char hostname[256];

//获取机器名
if(gethostname(hostname,256)<0)
{
perror("gethostname");
return;
}
//根据机器名解析地址
if((h=gethostbyname(hostname))==NULL)
{
perror("gethostbyname");
return;
}
//输出主机名
printf("host name: %s\n",h->h_name);
//循环输出所有的主机别名
for(i=0,p=h->h_aliases;*p!=NULL;p++,i++)
printf("alias name %d: %s\n",i+1,*p);
//循环输出所有的IP地址
for(i=0;i<h->h_length/sizeof(int);i++)
printf("IP address %d: %s\n",i+1,
   inet_ntoa(*((struct in_addr*)h->h_addr_list[i])));
}

说明:经过编译链接,在shell下运行上述程序,其输出结果如下所示:
host name: ljf-linux
alias name 1: localhost6.localdomain6
alias name 2: localhost6
IP address 1: 192.168.1.33

13.2.4 服务名称数据结构
        在Linux系统中存在一个网络服务配置文件 /etc/services,该文件中定义了当前主机提供的网络服务名称、对应的端口号和协议。文件的每一行定义了一种网络服务,每一行由4节组成,节间由空格分隔。其中,第一节指定了服务的名称,如telnet;第二节指定了服务的端口号和协议,端口号与协议用“/”分隔;第三节定义了服务的别名。这个文件的作用主要有两个:一是为Linux超级服务所使用;而是在进行网络编程时使用。在进行服务器网络编程中,需要定义服务绑定的端口号。如果直接将端口号写到程序中,那么一旦端口号发生变化,则需要修改程序并重新编译,这是所有程序员都不希望发生的事。此时,可以在/etc/services中进行更改即可。
        Linux系统提供了对/etc/services文件进行操作的一系列函数,如getservbyname、getservbyport等。这些函数都需要使用服务名称数据结构struct servent。该结构的定义在头文件netdb.h中,其声明如下:
/* Description of data base entry for a single service.  */
struct servent
{
  char *s_name; /* Official service name.  */
  char **s_aliases;/* Alias list.  */
  int s_port; /* Port number.  */
  char *s_proto; /* Protocol to use.  */
};
成员说明:
1)s_name:服务名称,如通常的telnet、ftp等。
2)s_aliases:服务的别名列表。
3)s_port:服务的端口号,如telnet定义在23端口,而ftp定义在21端口。
4)s_proto:协议名称,如TCP、UDP等。

例13-3:查看当前系统中telnet服务绑定的端口号。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netdb.h>
#include <error.h>

void main()
{
struct servent *sv=NULL;
//根据服务名称获取服务结构
sv=getservbyname("telnet","tcp");
if(sv==NULL)
{
perror("getservbyname");
return;
}
//输出该服务所对应的端口号
printf("port:%d\n",ntohs(sv->s_port));
}
说明:经过编译链接,在shell下运行上述程序,其输出结果如下所示:
port:23

13.2.5 通用数据收发结构
        套接口编程环境下,收据的收发可以通过函数send、recv进行,也可以通过sendto、recvfrom进行。除此之外,Linux还提供了两个通用数据收发函数sendmsg和recvmsg。这两个函数可以完成前面提到的函数的全部功能。在调用sendmsg和recvmsg时,需要用到一个类型为struct msghdr的结构,此处称其为通用数据收发结构。该结构的定义位于头文件<bits/socket.h>中,其声明如下所示:
/* Structure describing messages sent by
   `sendmsg' and received by `recvmsg'.  */
struct msghdr
{
    void *msg_name; /* Address to send to/receive from.  */
    socklen_t msg_namelen; /* Length of address data.  */

    struct iovec *msg_iov; /* Vector of data to send/receive into.  */
    size_t msg_iovlen; /* Number of elements in the vector.  */

    void *msg_control; /* Ancillary data (eg BSD filedesc passing). */
    size_t msg_controllen; /* Ancillary data buffer length.
   !! The type should be socklen_t but the
   definition of the kernel is incompatible
   with this.  */

    int msg_flags; /* Flags on received message.  */
};
成员说明:
1)msg_name:套接口名称。在PF_INET协议族中,该参数为指向套接口地址结构(struct sockaddr_in)的指针。
2)msg_namelen:msg_name的长度。
3)msg_ivo:读取或者接收数据缓冲区结构。该结构的定义如下:
struct iovec
{
    void *iov_base;    //输入或者输出缓冲区的指针
    size_t iov_len;    //要读取或者接收的数据的长度
}
4)msg_iovlen:msg_iov的元素个数。
5)msg_control:辅助数据。
6)msg_controllen:msg_control的尺寸。
7)msg_flags:标志。该参数只对recvmsg函数有效。如果在调用sendmsg时要使用标志值,需要使用sendmsg函数的参数flags,而不是msghdr结构中的msg_flags。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、






待续中..................

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值