怎么能让一台电脑上的shell脚本编程100例操作转到另一台的键盘上面

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

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

例4-1:创建一个C文件add.c,在文件中实现函数Add,并将Add函数添加到静态库libMyFuncs.a中。 add.c文件内容如下:

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


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

例4-3:打包发布函数库 创建头文件MyFuncs.h,内容如下:

方式2:通过dlopen系列函数。

此处通过方式2调用动态库函数,下面先介绍dlopen系列函数。

4.2.2.1 dlopen函数 dlopen函数打开动态库并映射到当前进程的内存,成功返回句柄,原型如下:

2)__mode:打开方式,有两种。第一种是RTLD_LAZY,只打开动态库文件,而暂时不解析外部对动态库的引用,直到动态库函数被执行时再解析对动态库的引用;第二种是RTLD_NOW,在dlopen返回前解析所有的对动态库的引用。

返回NULL,调用dlopen函数失败,可以调用dlerror获取详细的错误信息;返回其他值,打开成功,返回值为访问动态库的句柄。

4.2.2.2 dlsym函数 dlsym函数的功能是返回一个指向被请求入口点的指针,原型如下:

2)__name:要调用的函数名称。

返回NULL,调用dlsym失败,可以调用dlerror获取详细的错误信息;返回其他值,打开成功,返回值为指向要调用的函数的指针。

返回0,调用dlclose成功;返回其他值,调用dlclose失败。

4.2.2.4 dlerror函数 dlerror函数用于输出动态库操作过程中的错误信息。原型如下:

例4-5:使用dlopen系列函数调用动态库libMyFuncs.so中的函数。 创建main.c文件,在文件中输入如下代码:

注意:记得加-ldl选项,因为dlopen系列函数是放在动态库libdl.so中的 执行:

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

导读:由于本章篇幅较长,所以把它拆分为如下几节。

5.2 文件权限管理之UGO方式
5.3 文件编程的基本概念

5.4 文件基本操作编程

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

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

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

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

图5-1:UGO模式文件权限位

Linux对文件的权限与目录的权限的定义是不同的。文件的读权限是指可以读取文件的内容(通过执行命令查看),目录的读权限是指可以查看(执行ls命令)目录所包含的文件列表;文件的写权限是指可以对文件的内容进行修改(执行vi命令修改文件内容),而目录的写权限是指可以在目录中新建或者删除文件的权限;文件的执行权限是指可以在shell环境下运行该文件,而目录的执行权限是指可以访问该目录(通过cd命令进入目录)。
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的进一步资料,有待以后深入研究时再做笔记。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

文件描述符是一个非负整数,其取值范围是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。但是,Linux提供了一种特殊的机制,可以在文件的属主权限中设置一个标志。这个标志的作用是:当执行该文件时,将进程的有效用户ID设置为该文件所有者的用户ID。如果设置了该标志,那么当一个文件被执行时,其有效用户ID不再是调起该进程的用户ID,而是该文件的属主ID。这种机制就称作设置-用户-ID(suid)。同样,对于组的权限也可以设置这样的标志,称作设置-组-ID(sgid)。suid可以通过命令chmod

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

参数说明: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在当前目录下创建一个文件拥有者可读写的文件,并在文件中写入文本串。代码如下:

成功,返回值即为文件描述符;失败,返回-1。

注意事项:1)文件打开方式参数应在O_RDONLY、O_WRONLY、O_RDWR中选择一个,然后通过按位或“|”与其他的可选参数进行组合。2)打开方式参数为O_WRONLY|O_CREAT|O_TRUNC的open调用与creat调用是等价的。例5-2:使用open打开当前目录下的HelloWorld.txt文件,并在文件尾部追加字符串。代码如下:

参数说明:1)fd:文件描述符。2)buff:缓冲区,用于缓存read从文件中读取的数据。3)count:要读取的字节数。返回值:

若成功,返回实际读取的字节数;若已读到文件尾,返回0;若出错,返回-1。

参数说明:1)fd:文件描述符。2)buff:缓冲区,准备写入文件的数据。3)count:要写入的字节数。返回值:

若成功,返回实际写入的字节数;若出错,返回-1。出错的常见原因是磁盘已满或者已超过文件最大长度的限制。

参数说明:1)fd:要关闭的文件描述符。返回值:

若成功,返回0;若失败,返回-1。

参数说明:1)file:文件路径。返回值:

若成功,返回0;若失败,返回-1。

参数说明:1)fd:文件描述符。2)offset:文件相对偏移量,偏移的基准是由whence参数决定的。3)whence:文件偏移依据,取值如下:SEEK_SET:从文件头开始SEEK_END:从文件尾开始SEEK_CRU:从当前位置开始返回值:

返回移动后的文件指针(相对于文件头的偏移量)

注意事项: 1)文件偏移量可以大于文件的当前长度。在这种情况下,对文件的下一次写入将延长该文件,并在文件中构成一个空洞。位于文件中的空洞将会被自动设置为'\0'。

2)可采用这样的方法获取文件长度:

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

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

设置文件属主的系统调用有3个,分别是chown、fchown、lchown。chown用于修改文件的属主;fchown与chown的功能是相同的,但是,fchown是对已打开的文件进行修改;lchown用于修改符号链接本身的属主,而不是符号链接所指向的文件。系统调用的原型为: #include <sys/types.h>

参数说明: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的用户,执行命令“

5.5.2 设置文件权限(UGO模式)        在UGO模式下,对文件权限的修改是通过函数chmod及fchmod完成的。fchmod与chmod实现的功能是相同的。不过fchmod是对文件描述符进行操作。这两个系统调用的原型为:

1)file:文件路径。

2)fd:文件描述符。

3)mode:权限组合。

若成功,返回0;若失败,返回-1。

1)执行chmod的进程的有效用户ID必须等于文件的拥有者,或者是超级用户,才能修改文件权限信息。

2)调用chmod更改文件权限信息时,修改的是i结点的数据结构,并没有修改文件内容。修改完成后,通过执行ls命令可以看到文件的修改时间并没有变化。

3)实际上,在权限组合参数中,还有一个称作文件粘住位的参数:S_ISVTX。

        前面说到过文件权限屏蔽字umask,一般上,系统中该屏蔽字的默认值是0022(可以通过命令umask查看)。我们也可以通过umask系统调用编程改变这个默认值。umask系统调用的原型为:

1)mask:文件权限组合。

调用umask前的文件权限屏蔽字。

        前面介绍了设置文件权限的方法,但是,如何获取/测试一个文件的访问权限呢?Linux提供系统调用access满足这个需求。access可以对文件是否具有某种权限进行检测,其原型为:

1)file:文件路径。

2)mode:可以是下面取值之一,或者是它们的按位或组合。

如果当前进程具有所测试的权限,则返回0;否则,返回-1。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

5.6.1 文件状态的获取

        在程序设计中,经常需要获取文件的状态信息,如文件的类型、大小及创建时间等。这些信息的获取是通过系统调用stat、fstat和lstat实现的。它们的原型为:

1)file:文件路径。

2)fd:文件描述符。

3)info:指向stat结构体的指针,用于保存获取到的文件状态信息。32位Linux(2.6.35)的stat结构如下:

若成功,返回0;若失败,返回-1。

1)当stat函数对一个符号链接文件操作时,返回的是该符号链接指向的文件的信息。

2)fstat函数与stat函数功能相同,但操作对象不同,它是对文件描述符操作。

3)lstat函数类似于stat,但是当文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息。

4)文件类型的判断:Linux为用户提供了一组宏定义用于对文件的类型进行判断,这组宏定义是根据结构体stat的st_mode成员进行判断的。宏定义如下所示:

例5-7:读取HelloWorld.txt文件的状态,输出文件类型、大小、时间等信息。

//用于保存文件状态的变量

"星期日","星期一","星期二","星期三","星期四","星期五","星期六"

//输出最后状态改变时间

5.6.2 更改文件时间信息

对于系统中的每个文件,Linux在内核中记录了3个时间:文件数据存取时间(st_atime)、文件数据修改时间(st_mtime)、i节点状态改变时间(st_ctime)。其中,st_ctime由文件系统维护,用户程序不能对其进行修改;而对于st_atim和st_mtime,Linux提供了系统调用utime,通过utime应用程序能“恶意”修改文件的这两个时间记录。utime的原型为:

1)file:文件路径。

若成功,返回0;若失败,返回-1。

注意观察实验结果,实际情况是utime的执行会导致“访问”时间和i节点“状态改变”时间发生改变,只有“修改”时间能改成“1997年07月01日00时00分00秒”。

5.6.3 文件系统状态的获取

1)path:文件路径。

2)info:指向结构体struct statfs的指针,该结构体的定义如下:

若成功,返回0;若失败,返回-1。

例5-9:获取根文件系统的总大小、剩余空间及使用率。

//文件系统信息结构体变量

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

5.7.1 工作目录与用户主目录

        Linux用户在登录系统后,在使用系统的过程中时刻都处于某个目录之中,这个目录就称作工作目录或当前目录。工作目录是可以随时改变的。当前目录用“.”表示,其父目录用“..”表示。

        用户主目录是在创建用户时系统自动创建的,每个用户都有自己的主目录。查看用户主目录可以通过查看文件“/etc/passwd”完成,或者通过环境变量HOME的值获得。在日常应用中,用户可以通过“~”字符来引用自己的主目录。

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

1)path:目录路径。

2)mode:文件权限组合。

若成功,返回0;若失败,返回-1。

1)新建目录的实际权限是由mode参数与文件权限掩码umask共同决定的。

2)创建目录成功后,系统将在新的目录项下自动创建两个子目录:“.”和“..”,分别代表当前目录和父目录。

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

例5-10:在当前目录创建新目录newdir,并指定权限为0700。

        删除目录的系统调用为rmdir,如果目录的引用计数变为0,则释放目录占用的系统空间;否则,只是将目录的引用计数减一。其原型为:

1)path:目录路径。

若成功,返回0;若失败,返回-1。

1)只有在目录中除“.”和“..”外,没有其他文件或目录时,rmdir才能调用成功。前面介绍过的unlink系统调用可以删除非空目录,但是执行删除操作的用户必须具有超级用户权限。

例5-11:删除当前目录下的newdir目录。

        当前工作目录是进程的属性,而起始目录则是用户的属性。进程调用chdir或fchdir函数可以改变当前工作目录,它们的原型为:

1)path:目录路径。

2)fd:目录的文件描述符。

若成功,返回0;若失败,返回-1。

1)buff:保存得到的工作目录名称缓存。

2)size:缓存大小。

若成功,返回当前工作目录的名称;若失败,返回NULL。

例5-12:改变工作目录到newdir目录,并获取工作目录的名字。

1)path:目录路径。

2)dp:DIR结构体的指针。DIR结构体是Linux内核的目录文件结构定义,该结构对于用户来说是透明的,用户只需在程序中直接使用即可。

1)opendir:若成功,返回DIR指针;若失败,返回NULL。

2)readdir:系统调用将返回一个指针,其所指向的结构体里保存着目录中下一个目录项的相关数据。后续的readdir调用将返回后续的目录项,直到发生错误或到达目录尾,此时,readdir将返回NULL。其中struct dirent的定义如下:

3)rewinddir:系统调用将目录的读指针复位至第一个目录项。无返回值。

4)closedir:若成功,返回0;若失败,返回-1。

例5-13:递归遍历指定的目录,输出所有的目录和文件名字。

//子函数:递归遍历指定的目录,输出所有的目录和文件名字

//循环读取目录pDir中的目录项

//确保不是.和..目录

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

        在上一章中介绍了Linux系统下的文件操作,这些文件操作都是直接通过文件操作的系统调用(也称文件操作API)完成的。在Linux系统中,还提供了另外一种访问文件的方式:标准输入/输出库(以下简称标准I/O)。标准I/O库是在文件操作的系统调用基础上进行封装的,更加便于应用开发人员使用。

        使用文件操作API访问文件系统,是针对文件描述符进行的;而使用标准I/O库时,所操作的则是一个名为“流”的对象。对于每一个进程来说,系统默认将为其打开3个流:标准输入、标准输出和标准错误输出。在系统中分别定义了全局指针(参考stdio.h)stdin、stdout和stderr,它们内部分别指向文件描述符

。在默认情况下,程序从键盘读取stdin,将向stdout和stderr输出的信息显示到屏幕上。如果要改变这种默认的输入输出方式,需要使用一种名称为“输入/输出重定向”的技术。


        标准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系统调用手工刷新缓存。

1)fp:指向FILE结构(流对象)的指针。

2)buff:自定义缓冲区指针。如果该参数为空(NULL),表示设置流对象为无缓存的模式(_IONBF)。对于setbuf来说,该缓冲区的大小固定为BUFSIZ。而对于setbuffer,缓冲区的大小由参数size指定。

3)size:自定义缓冲区的大小,以字节为单位。

1)setvbuf:若成功,返回0;若失败,返回其他值。

1)要设置一个流为无缓存模式,只需调用setbuf(fp,NULL)即可。

例6-1:测试stdout的默认缓存模式输出,fflush刷新效果以及关闭缓存模式后的输出。

//关闭标准输出的缓存模式

这是标准错误输出1 这是标准输出1 

这是标准输出2 这是标准错误输出2 

这是标准输出3 这是标准错误输出3

6.3 输入输出重定向

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

例6-2:编程实现标准输出重定向,将标准输出重定向到工作目录的stdout.txt文件中。

//关闭标准输出设备,否则将会导致内存泄漏

//向标准输出设备输出测试信息,经过重定向,应该会输出到stdout.txt文件中

1)在本例中,首先打开要重定向的目标文件,然后关闭标准输出,并调用dup2复制目标文件的描述符至标准输出。经过如此处理后,在向标准输出设备输出信息时,将重定向至目标文件。

2)本例中的fileno系统调用的作用是取得某个流的文件描述符。其中,fileno(stdout)与直接使用1是等价的。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

导读:由于本章篇幅较长,所以把它拆分为如下几节。

7.1 进程的基本概念

7.2 进程的运行环境

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

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

        进程创建后,系统内核为其分配了一系列的数据结构。这些数据结构中保存了进程的相关属性。主要的进程属性包括以下几种。

1)进程的标识符(ID):进程创建时,内核为其分配一个惟一的标识符,该标识符是一个short类型的非负整数。进程ID是由系统循环使用的,如果当前可用进程号超过了最大值,将从0开始选择可用的整数继续循环使用。

2)父进程标识符:Linux下的全部进程组成一棵进程树,其中树根进程是0号进程swapper。除根进程外,每个进程都有其对应的父进程。

3)用户标识符:是指运行该程序的用户ID。

4)组标识符:是指运行该程序的用户所归属的组ID。

5)有效用户标识符:是指该进程运行过程中有效的用户身份。在进行文件权限许可检查时,以该有效用户标识为依据。

6)有效组标识符:是指该进程运行过程中有效的组标识。在进行文件权限许可检查时,以该有效组标识为依据。

7)进程组标识符:一个进程可以属于某个进程组。通过设置进程组,可以实现向一组进程发送信号等进程控制操作。

8)会话标识符:每个进程都属于惟一的会话。

1)pid:进程的标识符。

若失败,返回-1;若成功,返回获取到的进程属性信息。

7.1.2 进程的内存映像

        在系统内存映像中,进程主要包括代码段、数据段、BSS段、堆栈段,各元素的含义如下。

1)代码段:用来存放可执行文件的指令,是可执行程序在内存中的映像。对代码段的访问有严格安全检查机制,以防止在运行时被非法修改,代码段是只读的。

2)数据段:用来存放程序中已初始化的全局变量。

3)BSS段:用来存放程序中未初始化的全局变量。

4)堆(heap):用于存放进程在运行过程中动态分配的内存。

5)栈(stack):用于存放局部变量以及函数调用的现场。

        在Linux系统中,每个进程都惟一地归属于某个进程组。在shell环境中,一条Linux命令就形成一个进程组。这条命令可以只包含一个命令,也可以是通过管道符连接起来的若干命令。每个进程组都有一个组长进程。进程组的ID就是这个 组长的ID。当进程组内的所有进程都结束或者加入到其他进程组内时,该进程组就结束了。

1)pid:用于指定要修改的进程ID,如果该参数为0,则指当前进程ID。

2)pgid:用于指定新的进程组ID,如果该参数为0,则指当前进程ID。

若成功,返回0;若失败,返回-1。

1)setpgid函数用于修改某个进程(不一定是自身)的进程组。

2)setpgrp函数用于设置当前进程为进程组的组长。调用该函数后,将产生一个新的进程组,进程组的组长ID即为当前进程的ID。

        当用户登录一个新的shell环境时,一个新的会话就产生了。一个会话可以包括若干个进程组,但是这些进程组中只能有一个前台进程组,其他的为后台进程组。前台进程组通过其组长进程与控制终端相连接,接收来自控制终端的输入及信号。一个会话由会话ID来标识,会话ID是会话首进程的进程ID。

Linux提供了系统调用setsid用于产生一个新的会话。不过,调用setsid的进程应该保证不是某个进程组的组长进程。setsid调用成功后,将生成一个新的会话。新会话的会话ID是调用进程的进程ID。新会话中只包含一个进程组,该进程组只包含一个进程(调用setsid的进程),且该会话没有控制终端,也就是说该进程将会变成后台进程。setsid的原型为:

若失败,返回-1,典型的错误是调用进程是某个进程组的组长,此时错误码errno为EPERM;若成功,返回进程的进程组ID。

7.1.5 进程的控制终端

        在Linux系统中,每个终端设备都有一个设备文件与其相关联,这些终端设备成为tty。可以通过tty命令查看当前终端的名称。用户可以通过telnet远程登录到某个Linux系统,此时其实并没有真正的终端设备。这种情况下,Linux系统将为用户自动分配一个成为“伪终端”的终端设备,伪终端的设备文件名称类似/dev/pts/???。

在Linux进程环境中,有一个成为“控制终端”的概念。所谓控制终端,就是指一个进程运行时,进程与用户进行交互的界面。一个进程从终端启动后,这个进程的运行过程就与控制终端密切相关。可以通过控制终端输入/输出,也可以通过控制终端向进程发送信号。当控制终端被关闭时,该控制终端所关联的进程将收到SIGHUP信号,系统对该信号的缺省处理方式就是终止进程。

        进程是由操作系统内核调度运行的,在调度过程中,进程的状态是不断发生变化的。这些状态包括以下几种。

1)可运行状态(RUNNING):该状态有两种情况,一是进程正在运行;二是处于就绪状态,只要得到CPU就可以立即投入运行。

2)等待状态(SLEEPING):表明进程正在等待某个事件发生或者等待某种资源。该状态可以分成两类:可中断的和不可中断的。处于可中断等待状态的进程,既可以被信号中断,也可以由于资源就绪而被唤醒进入运行状态。而不可中断等待状态的进程在任何情况下都不可中断,只有在等待的资源准备好后方可被唤醒。

3)暂停状态(STOPPED):进程接收到某个信号,暂时停止运行。大多数进程是由于处于调试中,才会出现该状态。

4)僵尸状态(ZOMBIE):表示进程结束但尚未消亡的一种状态。一个进程结束运行退出时,就处于僵尸状态。进程会在退出前向其父进程发送SIGCLD信号,父进程应该调用wait为子进程的退出做最后的收尾工作。如果父进程未进行该工作,则子进程虽然已退出,但通过执行ps命令仍然可以看到该进程,这样的进程成为僵尸进程。

进程的优先级定义了进程被调度的优先顺序,优先级的数值越小,则进程的优先权越高。进程的优先级是由进程的优先级别(PR)和进程的谦让值(NI)两个因素共同确定的。内核在调度进程时,将优先级别(PR)和谦让值(NI)相加以确定进程的真正优先级。对于一个进程来说,其优先级别(PR)是由父进程继承而来的,用户进程不可更改,但是可以更改谦让值(NI)。进程的谦让值在进程被创建时置为默认值0,系统允许的谦让值范围为最高优先级的-20到最低优先级的19。

        为方便用户操作进程的优先级,Linux提供了几个系统调用以修改/获取进程的谦让值。这些系统调用的原型为:

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:修改当前进程的谦让值,完成后输出进程的谦让值。

//设置进程的谦让值为3

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

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

7.2.1 进程的入口函数

       众所周知,C语言的入口函数是main,进程开始执行时,都是从main函数开始的。所以,在一个可执行的Linux程序中,必须包含有main函数。main函数的完整原型为:

1)argc:程序执行时的命令行参数个数,该参数个数包含了程序名称本身。

2)argv:命令行参数数组,其中每个数组成员为一个命令行参数,程序名称对应该数组第一个成员argv[0]。各命令行参数是以空格分隔的。

3)env:环境变量数组,可以在程序中访问这些变量。

main函数的返回值可以在shell中通过命令echo $?获取。

1)main函数可以有多种格式,上面给出的是完成的格式。根据具体应用main函数还可以有如下格式:

例7-2:演示完整的main函数。

//返回命令行参数个数

        从上面的介绍可以知道,在编程时可以通过直接访问命令行参数数组的方法获取命令行参数。另外,Linux还提供了专门的系统调用getopt获取命令行参数。getopt提供更为强大的获取命令行参数的方法:可以按照规则解析命令行参数。如执行下述程序:./test_program -i -s abc -t,在这种情况下,要获取到选项-s的参数abc需要进行很复杂的判断;而通过getopt可以很简单地解决这个问题。getopt系统调用的原型为:

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:指向当前选项参数(如果有)的指针;

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:设计一个程序,读取用户的命令行输入,根据输入选项执行响应的输出,如果输入错误则提示错误信息。

//如果没有输入任何选项

//禁止getopt函数输出错误信息

//循环调用getopt函数解析命令行

//根据不同的选项作不同的处理

//如果碰到不认识的选项

//如果最后一个选项需要输入参数但用户没有输入

//成功地解析了用户输入的所有选项

name:多字符的选项名称。

flag:如果该成员定义为NULL,那么getopt_long的返回值为该结构val字段值;如果该成员不为NULL而是指向一个变量,那么getopt_long调用后将在所指向的变量中填入val值,并且getopt_long返回0。通常该成员定义为NULL即可。

val:该长选项对应的短选项名称。

5)longinddex:输出参数,如果该参数不为NULL,那么它是一个指向整型变量的指针,在getopt_long运行时,该整型变量会被赋值为获取到的选项在结构数组longopts中的索引值。

1)-1:解析失败或者解析完毕。

2)其他:返回解析成功的选项字符(val值)。

1)getopt_long在解析过程中也会涉及到4个全局变量otperr、optarg、optind和optopt,可以充分利用这几个变量以协助程序设计。

例7-4:编程实现长选项名字的解析。

//如果没有输入任何选项

//如果碰到不认识的选项

//如果最后一个选项需要输入参数但用户没有输入

//成功地解析了用户输入的所有选项

7.2.4 进程的环境变量

        在编程过程中,有时可以利用环境变量简化编程工作。Linux提供了两个系统调用getenv和putenv用于获取和设置环境变量,它们的原型为:

1)name:环境变量名称。

2)namevalue:要设置的环境变量串,格式为“环境变量名=值”。

1)getenv:返回NULL,表示相关的环境变量未定义;返回其它,环境变量的值。
2)putenv:返回0,成功;返回-1,失败。

例7-5:读取环境变量CONFIG_PATH的值,如果该环境变量不存在则创建。

//如果环境变量没有定义

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

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

        fork调用成功后,将并发出新的进程,此时,新生成的进程称为子进程,而原来的调用进程称为父进程。fork系统调用是非常特殊的一个系统调用,调用fork一次将返回两次,分别在父进程和子进程中返回。在父进程中,其返回值为子进程的进程标识符;在子进程中,其返回值为0。

        fork调用成功后,产生的子进程继承了父进程大部分的属性,这些属性主要包括以下几点:

1)进程的实际用户ID、实际用户组ID和有效用户ID、有效用户组ID。

2)进程组ID、会话ID及控制终端。

3)当前工作目录及根目录。

4)文件创建掩码UMASK。

6)父进程中已经打开的描述符(如文件描述符、套接口描述符等)。

        除此之外,也有一部分进程属性是不能直接从父进程那里继承的,主要包括以下几点:

2)用户时间和系统时间,这两个时间被初始化为0。

3)超时时钟设置为0,这个时钟是由alarm系统调用使用的。

4)信号处理函数指针组置为空。

返回值为pid_t类型,pid_t是一个宏定义,其实质是int,被定义在头文件<sys/types.h>中。fork调用成功后在父进程中返回子进程ID,在子进程中返回0;若出错,返回-1。

1)由于子进程继承了父进程中已打开的文件描述符,在这种情况下,这些描述符的引用计数已经加一(每fork一次就加一)。因此,在关闭这些描述符时,要记住多次关闭直至描述符的引用计数为0。

2)子进程复制了父进程的数据段,有其独立的地址空间。父子进程各有一份全局变量的拷贝。因此,不能通过全局变量在父子进程间进行通信,而要通过专门的进程间通信机制。

例7-6:编程创建多个进程,每个进程输出自己的相关信息。

//忽略SIGCLD信号,避免形成僵尸进程

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

        exec系列函数并不创建新进程,调用exec前后的进程ID是相同的。exec函数的主要工作是清除父进程的可执行代码映像,用新程序的代码覆盖调用exec的进程代码。如果exec执行成功,进程将从新程序的main函数入口开始执行。调用exec后,除进程ID保持不变外,还有下列进程属性也保持不变。

1)进程的父进程ID。

2)实际用户ID和实际用户组ID。

3)进程组ID、会话ID和控制终端。

4)定时器的剩余时间。

5)当前工作目录及根目录。

6)文件创建掩码UMASK。

exec系列函数共有6种不同的形式,统称为exec函数。为讲解清晰,把这6个函数划分为两组:一组是execl、execle和execlp;另一组是execv、execve和execvp。这两组函数的不同在于exec后的第一个字符,第一组是l,在此称为execl系列;第二组是v,在此称为execv系列。这里的l是list(列表)的意思,表示execl系列函数需要将每个命令行参数作为函数的参数进行传递。而v是vector(矢量)的意思,表示execv系列函数将所有函数包装到一个矢量数组中传递即可。exec函数的原型为:

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命令输出当前目录的文件列表。

//定义参数数组,为execv所使用

//在子进程中执行“ls -l”命令

//子进程调用execl后进程空间将被ls命令覆盖

//父进程继续创建子进程

//在子进程中执行“ls -a”命令

//子进程调用execv后进程空间将被ls命令覆盖

        为了方便地调用外部程序,Linux提供了system系统调用。与exec系统调用不同,system将外部可执行程序加载执行完毕后继续返回调用进程。system的返回值就是被加载的程序的返回值。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”命令输出当前目录的文件列表。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

进程执行完毕后,应该合理的终止,释放进程占用的资源。终止进程的方式有多种,可以是接收到其他进程发送的信号而被动终止进程,也可以是进程自己执行完毕后主动退出进程。本节介绍主动退出进程的exit函数。在Linux中,除调用exit可以结束进程外,还有另一个函数_exit也可以实现类似的功能。但是,由于_exit函数在退出时并不刷新带缓冲I/O的缓冲区,所以在使用带缓冲的I/O操作时,应该调用exit函数,而不是_exit。这两个函数的原型为:

1)status:该参数指定进程退出时的返回值,该返回值可以在shell中通过“echo $?”命令查看,也可以通过system函数的返回值取得,还可以在父进程中通过调用wait函数获得。

1)通常进程返回0表示正常退出(如exit(0)),返回非零表示异常退出(如exit(1)/exit(-1))。

        一个进程结束时将向其父进程发送SIGCLD信号,父进程可以忽略该信号或者安装信号处理函数处理该信号。而处理该信号通常需要调用wait系列函数。wait系列函数的作用是等待子进程的退出并获取子进程的返回值。通常情况下,wait函数是阻塞等待的,直到调用进程的某个子进程退出。wait系列函数的原型为:

1)status:用于保存子进程的结束状态。

2)pid:为欲等待的子进程识别码,其数值意义如下:

pid<-1:等待进程组识别码为 pid 绝对值的任何子进程;

pid=0:等待进程组识别码与目前进程相同的任何子进程;

pid>0:等待任何子进程识别码为 pid 的子进程。

3)options:该参数提供了一些额外的选项来控制waitpid,可有以下几个取值或它们的按位或组合:

WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。

WUNTRACED:若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。

2)其他:调用成功,返回值为退出的子进程ID。

1)Linux提供了多个宏以便从该结束状态中获取特定信息,具体信息如下:

WIFSIGNALED(status):若为异常结束子进程返回的状态,则为真;对于这种情况可执行WTERMSIG(status),取使子进程结束的信号编号。

WTERMSIG(status) :取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED 来判断后才使用此宏。

WIFSTOPPED(status) :若为当前暂停子进程返回的状态,则为真;对于这种情况可执行WSTOPSIG(status),取使子进程暂停的信号编号。

例7-9:编程实现监控子进程退出。

//调用wait等待子进程退出

//判断子进程退出时是否有返回值

//输出子进程的返回值

//判断子进程是否被信号中断而结束

//输出中断子进程的信号

//父进程休眠1秒,等待子进程退出。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

导读:由于本章篇幅较长,所以把它拆分为如下几节。

8.1 信号的基本概念

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

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

        只要具备相应权限,进程之间可以相互通过系统调用kill发送信号。操作系统内核也可能因为内部事件而给进程发送信号,通知进程有某个事件发生。因此,信号的来源可能是系统内核,也可能是其他的进程。引起信号产生的原因基本上包括以下几种。

1)程序中执行错误的代码。如内存访问越界、数学运算除零等。

2)其他进程发送来的信号。

3)用户通过控制终端发送来的信号。最常见的情况是:一个程序在运行中,用户通过键盘输入<Ctrl>+<C>键或者<Ctrl>+<\>键终止程序的执行。

4)子进程结束时向父进程发送的SIGCLD信号。

5)程序中设定的定时器产生的SIGALRM信号。

        为方便地标识每一种信号,Linux系统为每种信号都分配了名字。这些名字都以SIG开头进行定义。可以通过执行命令kill -l获得系统中全部信号的列表,在Ubuntu-10.10系统中,信号列表如下所示。

上面这些信号的定义在头文件<asm/signal.h>中。Linux系统中的信号可以划分为可靠信号和不可靠信号。可靠信号是指信号一旦发出,操作系统保证该信号不会丢失;而不可靠信号由于内核不对信号进行排队,造成的后果就是信号有可能丢失。在Linux的信号中,编号值是1~31(SIGHUP~SIGSYS)的信号是不可靠信号,而编号为32~64的信号是可靠信号。

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:子进程退出信号。在一个进程创建一个或者若干个子进程后,每个子进程退出时,父进程都将收到该信号。如果父进程没有忽略该信号,也没有处理该信号,则子进程退出后将形成僵尸进程。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

        在用户的应用程序中,可以自行安装信号,定义进程收到信号后的处理方法。这一过程是通过系统调用signal或者sigaction完成的。本节介绍signal系统调用,下一节介绍sigaction系统调用。signal系统调用的原型为:

1)sig:要安装的信号值。

2)handler:指向信号处理函数的指针。除自行指定处理函数指针外,该参数还可以有以下选择。

SIG_DFN:采用缺省的信号处理方式;

若成功,返回信号处理函数的指针;若失败,返回SIG_ERR。

//输出接收到的信号信息

输入:同时按下<Ctrl>+<C>组合键中止程序运行。注意观察控制台输出。

        signal可以实现基本的信号安装功能,但在某些情况下,可能需要对信号的安装进行更多的控制。此时就需要使用sigaction系统调用。sigaction的原型为:

1)sig:要安装的信号值。

2)act:指定安装信号的数据结构。该参数是一个struct sigaction类型的指针。该结构的定义位于头文件<bits/sigaction>中,如下所示。

3)oact:输出参数,指向struct sigaction结构的指针。调用sigaction成功后,该参数中将返回信号的原来处理方式。如果不需要获取信号的原来处理方式,则该参数可以传入NULL。

若成功,返回0;若失败,返回-1。

1)用signal安装的信号,无法在发送信号时附加其他数据。而sigaction函数通过SA_SIGINFO标志安装的信号,则可以实现这一点。

例8-2 用sigaction为进程安装SIGINT信号,要求在进程发送信号时可以附加数据。

//信号安装时需要用到的变量

//设置发送信号时可以附加数据

输入:同时按下<Ctrl>+<C>组合键中止程序运行。注意观察控制台输出。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

8.3.1 信号的处理方式

        在Linux系统下,信号的处理方式有3种:一是忽略信号;而是按照系统提供的缺省处理规则进行处理;三是捕捉信号,在程序中定义自己的信号处理函数,在信号处理函数中完成相应的功能。Linux系统分别为前两种方式提供了相应的宏定义:SIG_IGN和SIG_DFN。下面列出几个重要信号及其处理方式。

 控制终端挂起或者退出

 不能阻塞、忽略、捕捉

 子进程退出时向父进程发送该信号

8.3.2 信号的阻塞处理

        信号的阻塞就是通知系统内核暂时停止向进程发送指定的信号,而是由内核对进程接收到的相应信号进行缓存排队,直到进程解除对相应信号的阻塞为止。一旦进程解除对该信号的阻塞,则缓存的信号将被发送到相应的进程。

1)系统自动阻塞:在信号的处理函数执行过程中,该信号将被阻塞,直到信号处理函数执行完毕,该阻塞将会解除。这种机制的作用主要是避免信号的嵌套。

2)通过sigaction实现人为阻塞:在使用sigaction安装信号时,如果设置了sa_mask阻塞信号集,则该信号集中的信号在信号处理函数执行期间将会阻塞。这种情况下进行信号阻塞的主要原因是:一个信号处理函数在执行过程中,可能会有其他信号到来。此时,当前的信号处理函数就会被中断。而这往往是不希望发生的。此时,可以通过sigaction系统调用的信号阻塞掩码对相关信号进行阻塞。通过这种方式阻塞的信号,在信号处理函数执行结束后就会解除。

3)通过sigprocmask实现人为阻塞:可以通过sigprocmask系统调用指定阻塞某个或者某几个信号。这种情况下进行信号阻塞的原因较多,一个典型的情况是:某个信号的处理函数与进程某段代码都要某个共享数据区进行读写。如果当进程正在读写共享数据区的过程中,一个信号过来,则进程的读写过程将被中断转而执行信号处理函数,而信号处理函数也要对该共享数据区进行读写,这样共享数据区就会发生混乱。这种情况下,需要在进程读写共享数据区前阻塞该信号,在读写完成后再解除该信号的阻塞。

提示:在信号的接收过程中可能存在这样的情况:若干个相同的信号同时到达。通过上面的介绍可以知道,当信号处理函数正在执行时,同类信号将被阻塞处理。但是,如果此时信号处理函数还没有来得及执行,那么该同类信号就不会阻塞,在这种情况下,将会发生一种成为“信号合并”的现象。同时到达的同类信号将被合并处理,就像只有一个信号到达一样。

        被阻塞的信号的集合成为当前进程的信号掩码。每个进程都有惟一的信号掩码。为了对信号进行阻塞或者解除阻塞,Linux提供了专门的系统调用sigprocmask完成这一任务。该函数的原型为:

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秒,再解除该信号的阻塞。

//信号掩码结构变量,用于指定新的信号掩码

//信号掩码结构变量,用于保存原来的信号处理掩码

//向掩码结构中增加信号SIGINT

编译运行该程序,在进程休眠期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,注意观看信号是否被阻塞。在休眠结束后,验证刚才被阻塞的SIGINT信号是否被重新发送。

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

        通过上节对信号阻塞的介绍可以知道,信号的阻塞实际上是对一个集合的操作。这个集合中可能包含多种信号,这就是信号集。信号集的数据类型为sigset_t,实际上是个结构体,它的定义如下所示。

1)set:输入参数,信号集。

2)signo:输入参数,要增加或删除或判断的信号。

1)对于sigismember函数:返回1表示信号属于信号集;返回0表示信号不属于信号集。

2)对于其他函数:若成功,返回0;若失败,返回-1。

8.3.4 未决信号的处理

信号的未决是信号产生后的一种状态,是指从信号产生后,到信号被接收进程处理之前的一种过渡状态。由于信号的未决状态时间非常短,所以通常情况下,处于未决状态的信号非常少。如果程序中使用了sigprocmask阻塞了某种信号,则向进程发送的这种信号将处于未决状态。Linux提供了专门的函数sigpending获取当前进程中处于未决状态的信号。该函数的原型为:

1)set:输出参数,处于未决状态的信号集。

若成功,返回0;若失败,返回-1。

例8-4:编程实现下面功能:为进程安装SIGINT信号,先阻塞该信号,休眠10秒,最后查看当前进程未决的信号。

//信号掩码结构变量,用于指定新的信号掩码

//信号掩码结构变量,用于保存原来的信号处理掩码

//信号掩码结构变量,用于保存未决的信号集

//向掩码结构中增加信号SIGINT

//获取当前未决的信号集

//判断SIGINT是否在未决信号集中

编译运行该程序,在进程休眠期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证该信号是否处于未决状态。

        在有些情况下,程序需要暂停执行,进入休眠状态,以等待信号的到来。这时可以使用pause系统调用。pause一旦被调用,则进程将进入休眠状态。之后,只有在进程接收到信号后,pause才会返回。pause的原型为:

例8-5:用pause编程实现等待SIGINT信号到来的功能。

编译运行该程序,在进程pause期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证该信号是否被处理。

注意:由于此测试程序没有屏蔽其他信号,因此任何一个信号的到来都能唤醒pause。

        pause系统调用可以实现暂停进程的执行等待某个信号的到来,但是,如果在pause被调用之前,指定的信号到达进程,那么,在随后的pause调用中,假定不再有信号到来,则进程将进入无限期的等待中。为此Linux提供了功能更强大的sigsuspend以满足这种需求。sigsuspend的工作过程如下:

1)设置进程的信号掩码并阻塞进程。

2)收到信号,恢复原来的信号掩码。

3)调用进程设置的信号处理函数。

4)等待信号处理函数返回后,sigsuspend返回。

        上述四个步骤是一次性完成的,操作系统保证操作过程的原子性。特别需要注意的是第三步调用信号处理函数是由sigsuspend完成的。sigsuspend的原型为:

1)set:输入参数,执行sigsuspend过程中需要被阻塞的信号集。

//信号掩码结构变量,用于指定新的信号掩码

//设置信号集为所有信号,准备阻塞所有信号

//从信号集中删除SIGINT信号,该信号为目标信号,不能被阻塞。

编译运行该程序,在进程suspend期间,按下<Ctrl>+<C>键向进程发送SIGINT信号,验证该信号是否被处理。

注意:由于此测试程序屏蔽了除SIGINT外的所有其他信号,因此只有在收到SIGINT信号后进程才会退出。

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

8.3.6 信号处理函数的实现

        信号处理函数是进程接收到信号后要执行的函数,该函数应该尽量简洁,一般不要执行过多的代码。最好只是改变一个外部标志变量的值,而在另外的程序中不断的检测该变量,繁杂的工作都留给那些程序去做。在定义信号处理函数时,应该特别注意以下几点。

1)如果信号处理程序中需要存取某个全局变量,则应该在程序中使用关键字volatile声明此变量。通知编译器,在编译过程中不要对该变量进行优化。

2)如果在信号处理函数中调用某个函数,那么那么该函数必须是可重入的,或者保证在信号处理函数执行期间不会有信号到达进程。Linux系统下存在许多不可重入的函数,如malloc、gethostbyname等。

在信号处理函数里,有时需要用到长跳转的操作。所谓长跳转,就是从信号处理函数直接跳转到函数体外指定的代码位置继续运行。Linux系统提供了两个函数实现该功能:设置跳转点的sigsetjmp和执行跳转的siglongjmp。sigsetjmp用来设置跳转点,在成功调用后,sigsetjmp语句所在的位置就是跳转点,这个位置指针将被保存到sigsetjmp的第一个参数中。这个两个函数的原型为:

1)env[1]:输出参数,该参数实际上是一个结构体的指针。该结构体中包含了长跳转指针,是否保存信号掩码及保存的信号掩码值等信息。对于应用人员来说,该结构是透明的。

2)env:输入参数,等效于env[1]。

3)savemask:是否保存信号掩码。如果该参数非零,则在调用sigsetjmp后,当前进程的信号掩码将被保存;在调用siglongjmp时,将恢复由sigsetjmp保存的信号掩码。

4)val:当由siglongjmp调用sigsetjmp时,该参数将会被隐含传给sigsetjmp作为返回值。如果val等于0,那么sigsetjmp函数将忽略该参数而返回其他非零值。

例8-7:编程实现捕捉SIGINT信号,在信号处理函数中用长跳转跳转至主程序。

//全局变量,用于保存跳转点及其现场

//从信号处理函数中跳转过来时,sigsetjmp将返回非零值

//输出提示信息后退出进程

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

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

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

        kill系统调用可以实现信号的发送,它既可以向单个进程发送信号,也可以向一组进程发送信号。kill的原型为:

1)pid:目标进程ID。通过传递不同pid值,kill可以实现多种信号发送方式。pid主要取值如下。

pid>0:将信号传给进程识别码为pid 的进程。

pid=0:将信号传给和目前进程相同进程组的所有进程

pid=-1:将信号广播传送给系统内所有的进程

pid<0:将信号传给进程组识别码为pid绝对值的所有进程

若成功,返回0;若失败,返回-1。

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

//输出接收到的信号信息

//输出当前进程的PID

        通过kill可以实现信号的发送。有些情况下需要在发送信号的同时附加其他数据,这时kill无法满足要求。Linux为此而提供了另一个系统调用sigqueue。该系统调用不仅可以实现kill的全部功能,而且还可以实现支持附加数据的发送。sigqueue的原型为:

1)pid:目的进程ID。与kill不同,sigqueue只能向一个进程发送信号。

2)signo:要发送的信号值

可选中1个或多个下面的关键词,搜索相关资料。也可直接点“搜索资料”搜索整个问题。

使用 read 命令暂停,等待用户输入,不作任何后续判断即可,这样任意键都可以继续。

我要回帖

更多关于 shell脚本编程100例 的文章

 

随机推荐