基于LINUX的操作系统实验教程(2)

更新时间:2023-12-26 16:27:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

前言

操作系统是计算机系统中的核心软件。操作系统教学不但需要讲授操作系统概念、原理与方法,还需要让学生进行编程实践,只有这样才能让学生真正理解操作系统的精髓。

Linux操作系统是源码公开的实用的现代操作系统,同时Linux也得到了广泛的普及,所以采用Linux作为操作系统实验的平台。

第1章 Linux系统概述

1.1.关于Linux

Linux于1991年诞生于芬兰。大学生Linus Torvalds,编写了一个小的操作系统内核,这就是Linux的前身。Linus Torvalds将操作系统的源代码在Internet上公布,受到了计算机爱好者的热烈欢迎。各种各样的计算机高手不断地为它添加新的特性,并不断地提高它的稳定性。1994年, Linux 1.0 正式发布。现在,Linux已经成为一个功能强大的32位的操作系统。

1984年,由Richard Stallman组织成立了Free Software Foundation(FSF)组织以及GNU项目,并不断地编写创建GNU程序(程序的许可方式均为GPL: General Public License)。GNU项目的目的是提供一个免费的类Unix的操作系统以及在上面运行的应用程序。

GNU项目在初期进展并不顺利,特别是操作系统内核方面。Linux适时而出,由于它出色的性能,使它成为GNU项目的操作系统的内核。从此以后, GNU项目进展非常迅速:全世界的计算机高手已经为它贡献了非常多的应用程序和源代码。

严格地说, Linux只是一个操作系统内核。比较正式的称呼是GNU/Linux操作系统,它使用Linux内核。GNU的意思是GNU’s not Unix(GNU不是Unix)—一种诙谐的说法,意指GNU是一种类Unix的操作系统。

为了保证GNU的软件可以自由使用和拷贝,GNU组织制订了一个新的法律许可协议:GPL协议。

该协议的主要特点: 允许软件被自由地拷贝

1

允许软件被自由地修改

允许软件被修改后自由地传播,但必须提供源代码。

很多软件制作者都遵循GPL协议,无数的软件开发人员和软件爱好者将自己的软件通过GPL分布,公布在互联网上,从而形成了一个庞大的GNU社区。

Linux是遵从GPL协议的软件,也就是说,只要遵从GPL协议,就可以免费得到它的软件和源代码,并对它进行自由地修改。

然而,对一般用户来说,从Internet或者其他途径获得这些源代码,然后对它们进行编译和安装是技术难度很高的工作。一些应用程序的安装也都非常复杂。因而,有一些公司如Red Hat、VA等开始介入Linux的业务。它们将Linux操作系统以及一些重要的应用程序打包,并提供较方便的安装界面。同时,还提供一些有偿的商业服务如技术支持等。这些公司所提供的产品一般称为Linux的发布版本。

目前比较著名的Linux发布版本有以下几种:

Red Hat—最著名的Linux服务提供商,Intel、Dell等大公司都对其有较大投资,该公司收购了开放源代码工具供应商CyGNUs公司。

Red Hat最早由Bob Young和Marc Ewing在1995年创建。而公司开始真正步入盈利时代,归功于收费的Red Hat Enterprise Linux(RHEL,Red Hat的企业版)。而正统的Red Hat版本早已停止技术支持,最后一版是Red Hat 9.0。于是,Red Hat分为两个系列:由Red Hat公司提供收费技术支持和更新的Red Hat Enterprise Linux,以及由社区开发的免费的Fedora Core。Fedora Core 1发布于2003年年末,而FC的定位便是桌面用户。FC提供了最新的软件包,同时,它的版本更新周期也非常短,仅六个月。

Fedora 是一个操作系统和平台,基于 Linux。它允许任何人自由地使用、修改和重发布,无论现在还是将来。它由一个庞大的社群开发,这个社群的成员以自己的不懈努力,提供并维护自由、开放源码的软件和开放的标准。Fedora Core 是 Fedora Project 的一部分,得到了 Red Hat, Inc. 的支持。

可运行的体系结构包括 x86, x86_64 和 PPC。 SlackWare—历史比较悠久,有一定的用户基础。 SUSE—在欧洲知名度较大。

Turbo Linux—在亚洲,特别是日本用户较多。该公司在中国推出了TurboLinux 4.0、4 .0 2和6 .0的中文版,汉化做得很出色。

Debain—完全由计算机爱好者和Linux社区的计算机高手维护的Linux发布版本。

Linux进入中国后,在我国计算机界引起了强烈的反响,也出现了许多汉化的

2

Linux发布版本,影响较大的有以下几种:

Xteam Linux—北京冲浪平台公司推出的产品,中国第一套汉化的Linux发布版本。

BluePoint—1999年底正式推出的产品,内核汉化技术颇受瞩目。 红旗Linux—中国科学院软件研究所和北大方正推出的Linux发布版本。 从本质上来说,上面所有发布版本使用的都是同样的内核(或者版本略有不同),因而,它们在使用上基本上没有什么区别。但它们的安装界面不一样,所包含的应用程序也有所不同。

Linux之所以大受欢迎,不仅仅因为它是免费的,而且还有以下原因: 1) Linux是一个真正的抢占式多任务、多线程、多用户的操作系统。 2) Linux性能非常稳定,功能强劲,可以与最新的商用操作系统媲美。 3) Linux有非常广泛的平台适应性。它在基于Intel公司的x 8 6(也包括AMD、Cyrixx、IDT)的计算机、基于Alpha的计算机,以及苹果、Sun、SGI等公司的计算机上都有相应的发布版本,甚至在AS/400这样的机器上都能找到相应的版本。Linux还可以在许多PDA和掌上电脑以及嵌入式设备上运行。

4) 已有非常多的应用程序可以在Linux上运行,大多数为SCO Unix开发的应用程序都能在Linux上运行(借助于i B C S软件包),甚至还比在SCO Unix上运行速度更快。借助Dosemu,可以运行许多DOS应用程序,而借助Wabi或Wine,还可以运行许多为Windows设计的软件。

5) Linux是公开源代码的,也就是说,不用担心某公司会在系统中留下后门(软件开发商或程序员预留的,可以绕开正常安全机制进入系统的入口)。

6) 只要遵从GPL协议,就可以自由地对Linux进行修改和剪裁。

当然, Linux的优点决不止于此。对计算机专业人员来说, Linux及其相关应用程序也是学习编程的绝好材料,因为这些软件都提供了完整的源代码。

Linux的出现为我国软件产业赶超世界先进水平提供了极好的机遇,也为我国软件产业反对微软的垄断提供了有力的武器。

1.2 关于shell

Shell是一种具备特殊功能的程序,它是介于使用者和 UNIX/Linux 操作系统之核心程序(kernel)间的一个接口。操作系统是一个系统资源的管理者与分配者,当用户有需求时,得向系统提出;从操作系统的角度来看,它也必须防止使用者因

3

为错误的操作而造成系统的伤害。众所周知,对计算机下命令得通过命令(command)或是程序(program);程序有编译器(compiler)将程序转为二进制代码,可是命令呢?其实shell 也是一个程序,它由输入设备读取命令,再将其转为计算机可以了解的机器码,然后执行它。

各种操作系统都有它自己的 shell,以 DOS 为例,它的 shell 就是 command.com 文件。如同 DOS 下有 NDOS,4DOS,DRDOS 等不同的命令解译程序可以取代标准的 command.com ,UNIX 下除了 Bourne shell(/bin/sh) 外还有 C shell(/bin/csh)、Korn shell(/bin/ksh)、Bourne again shell(/bin/bash)、Tenex C shell(tcsh) ? 等其它的 shell。UNIX/Linux 将 shell 独立于核心程序之外,使得它就如同一般的应用程序,可以在不影响操作系统本身的情况下进行修改、更新版本或是添加新的功能。

(1)Shell 的激活

在系统启动的时候,核心程序会被加载内存,负责管理系统的工作,直到系统关闭为止。它建立并控制着处理程序,管理内存、档案系统、通讯等等。而其它的程序,包括 shell 程序,都存放在磁盘中。核心程序将它们加载内存,执行它们,并且在它们中止后清理系统。Shell 是一个公用程序,由使用者login时启动,Shell 提供使用者和核心程序产生交谈的功能。

当用户(login)时,一个交谈式的shell 会跟着启动,并提示用户输入命令。在用户键入一个命令后,接着就是 shell 的工作了,它会进行:

1. 语法分析命令列

2. 处理万用字符(wildcards)、转向(redirection)、管线(pipes)与工作控制(job control)

3. 搜寻并执行命令

当用户刚开始学UNIX/Linux系统时,大部分的时间会花在于提示符号(prompt)下执行命令。

如果用户经常会输入一组相同形式的命令,可能会想要自动执行那些工作。如此,可以将一些命令放入一个文件(称为script),然后执行该文件。一个shell 命令文件很像是 DOS 下的批处理文件(如 Autoexec.bat):它把一连串的 UNIX 命令存入一个文件,然后执行该文件。较成熟的命令文件还支持若干现代程序语言的控制结构,如条件判断、循环、测试、传送参数等。要写命令文件,不仅要学习程序设计的结构和技巧,而且要对 UNIX/Linux 公用程序及如何运作需有深入的了解。有些公用程序的功能非常强大(例如 grep、sed 和awk),它们常被用于命令文件来操控命令输出和档案。在用户对那些工具和程序设计结构变得熟悉之后,就可以开

4

始写命令文件。当由命令文件执行命令时,此刻,就已经把 shell 当做程序语言使用了。

(2)Shell 的生平

第一个有重要意义的,标准的 UNIX shell 是V7(AT&T的第七版)UNIX,在1979 年底被提出,且以它的创造者 Stephen Bourne 来命名。Bourne shell 是以 Algol 这种语言为基础来设计,主要被用来做自动化系统管理工作。

C shell 是在加州大学柏克来分校于70年代末期发展而成,而以2BSD UNIX的部分发行。这个 shell 主要是由 Bill Joy 写成,提供了一些在标准 Bourne shell 所看不到的额外特色。C shell 是以C 程序语言作为基础,且它被用来当程序语言时,能共享类似的语法。它也提供在交谈式运用上的改进,例如命令列历程、别名和工作控制。因为 C shell 是在大型机器上设计出来,且增加了一些额外功能,所以 C shell 有在小型机器上跑得较慢,即使在大型机器上跟 Bourne shell 比起来也显得缓慢。

有了 Bourne shell 和 C shell 之后,UNIX 使用者就有了选择,且争论那一个 shell 较好。AT&T 的David Korn 在 80 年代中期发明了 Korn shell,在 1986 年发行且在 1988 年成为正式的部分 SVR4 UNIX。Korn shell 实际上是 Bourne shell 的超集,且不只可在 UNIX 系统上执行,同时也可在 OS/2、VMS、和 DOS上执行。它提供了和 Bourne shell 向上兼容的能力,且增加了许多在 C shell 上受欢迎的特色,更增加了速度和效率。 Korn shell 已历经许多修正版,要找寻您使用的是那一个版本可在 ksh 提示符号下按 Ctrl-v 键。

三种主要的 Shell 与其分身

在大部份的UNIX系统,三种著名且广被支持的shell 是Bourne shell(AT&T shell,在 Linux 下是BASH)、C shell(Berkeley shell,在 Linux 下是TCSH)和 Korn shell(Bourne shell的超集)。这三种 shell 在交谈(interactive)模式下的表现相当类似,但作为命令文件语言时,在语法和执行效率上就有些不同了。

Bourne shell 是标准的 UNIX shell,以前常被用来做为管理系统之用。大部份的系统管理命令文件,例如 rc start、stop 与shutdown 都是Bourne shell 的命令档,且在单一使用者模式(single user mode)下以 root 登录时它常被系统管理者使用。Bourne shell 是由 AT&T 发展的,以简洁、快速著名。 Bourne shell 提示符号的默认值是 $。

C shell 是柏克莱大学(Berkeley)所开发的,且加入了一些新特性,如命令列历程(history)、别名(alias)、内建算术、档名完成(filename completion)、和工作控制(job control)。对于常在交谈模式下执行 shell 的使用者而言,他们较喜

5

cat命令通常会从命令行给定的文件中读取内容显示在屏幕上,但当命令行没有给出文件时,它将从标准输入文件,即键盘输入中读取信息显示在屏幕上。

如:cat

12.Linux的输入输出重定向

? 输入重定向:就是把命令的标准输入重新定向为指定的文件。

例如wc 命令统计指定文件包含的行数、单词数和字符数。wc /etc/passwd 若仅在命令行上键入 wc,wc将等待用户输入信息,且键盘输入的信息出现在屏幕上,直到用户按下ctrl+d,wc才将统计结果显示在屏幕上。

另一种将指定文件传给wc的方式是使用重定向。 如wc

由于大多数命令都以参数的形式在命令行上指定输入文件的文件名,所以输入重定向并不经常使用。

但要使用一个不接收文件名作输入参数的命令,而需要的输入内容又存在于一个文件中,这时就可以使用输入重定向。

? 输出重定向:就是把命令的标准输出重新定向到指定的文件中。 例如 ls -l >d.out 或ls –l >>d.out 13.管道

管道 |:将一个程序或命令的输出作为另一个程序或命令的输入。 例如:cat sample.txt|grep “High”|wc –l

管道将cat的输出送给grep命令,该命令在输入中查找包含单词“High”的行,这个输出又送给wc命令,该命令统计输入中的行数。

14.文件的打包和压缩命令

从internet上下载文件时,很多文件都是打包或压缩文件,例如:wb.txt.gz, longkey.tar.gz

相关命令:zip ,gzip, tar 例如:

(1)把/home/longkey目录下的所有文件和子目录备份到longkey.tar文件

11

中。

tar –cvf longkey.tar /home/longkey

从longkey.tar文档中恢复数据,放在当前目录下。

tar –xzf longkey.tar.gz

(2)把/home/longkey目录下的所有文件和子目录以gzip压缩文件的形式备份到longkey.tar.gz文件中。

15.改变文件的存取权限命令

改变文件存取权限的用户只能是root用户或文件主用户

命令:chmod [who] operator [pemission] filename(符号模式)

或chmod mode filename(绝对模式)

who (u ,g, o,a) Operator(+,-,=) Pemission(r,w,x,s,t)

例如:chmod u=rwx,g+w,o+r myfile chmod u+s,g+x,o+x myfile

组用户和其它用户执行这个文件myfile时,在运行中具有用户主权限。

16.组和用户的管理 用户管理

每个用户都有一个数值与之对应,称为UID。

LINUX下的用户可以分为三类:超级用户、系统用户和普通用户。

超级用户用户名:root, 具有一切权限。一般只有在进行系统维护(如创建用系统用户:是LINUX系统正常工作所必须的内建的用户,主要是满足相应的系普通用户:大多数用户属于此类。

tar –czvf longkey.tar.gz /home/longkey tar –xzf longkey.tar.gz

从longkey.tar.gz文档中恢复数据,放在当前目录下。 注意:在释放文件时,若要指定目录,可加-C 目录名选项

户)和其它必要的情况下才使用超级用户身份登录,以避免系统出现安全问题。 统进程对文件属主的要求而建立的,系统用户不能用来登录。

12

超级用户UID:0 系统用户UID:1---499 普通用户UID:500---60000

关于用户的信息,LINUX放在文件/etc/passwd中。

组的管理

? LINUX的组有私有组、系统组、标准组。

? 私有组:建立用户帐号时,若没有指定帐号所属的组,系统会建立一个组名和帐号名相同的组,称为私有组。该组只容纳一个用户 ? 标准组可容纳多个用户。

? 系统组是LINUX系统正常运行所必须的,安装系统或添加新的软件包会自动建立系统组。

? LINUX关于组的信息放在文件/etc/group中 例如:cat /etc/group

相关的命令:

添加一个新组:groupadd 命令 组属性的修改:groupmod 命令

显示/etc/group文件的内容,解释每一行每一列的含义

创建新的用户 useradd 修改用户密码passwd 改变用户的属性 usermod

显示/etc/passwd文件,解释每一行每一列的含义

注:这些命令都可以通过查看联机帮助学习

17.建立链接ln命令

Ln [参数] 源文件或目录 链接名

-s参数:建立符号链接。不加参数建立的是硬链接。 例如:ln /home/test.txt test1

在当前目录下建立/home目录下的test.txt文件的硬链接,链接名为test.

13

命令ls –li /home

在文件列表中可以看到/home目录下的test.txt文件的链接计数增1。 ln –s /home/test.txt test2

在当前目录下建立/home目录下的test.txt文件的符号链接,链接名为test2。 用ls –li显示当前目录下的文件列表,注意test1和test2文件的不同。

18.进程的启动

进程的启动有两种方式: 手工启动和调度启动。

手工启动又分为前台启动和后台启动。

用户直接运行一个程序或执行一个命令时,就启动了一个新的前台进程。 前台进程的特点:进程不结束,终端不出现提示符。

有些进程耗时长,用户不着急等待结果,就可以在运行的程序或启动的命令后加&,表示以后台方式运行程序或执行命令。如 /root/test & 以后台方式运行test程序。

后台进程的特点,当进程启动后,不必等待进程结束,终端马上会出现提示符。

19.报告进程状态的ps命令 语法:ps [选项] -e:显示所有进程 -f:全格式 -h:不显示标题 -l:给出长列表

-a:显示终端上的所有进程,包括其他用户的进程。

查看当前进程的情况。通常可用ps命令监视后台进程的工作情况。因为后台进程是不与屏幕、键盘这些标准输入输出设备进行通信的,所以需要检测其情况,可使用ps命令。

例如: locate lib |less & ps

(locate命令查找绝对路径中包含指定字符串的文件,less命令逐页显示文件中的内容,|为管道,locate指令的输出作为less指令的输入。&表示以后台方式运行命令。)

14

20.发送信号的kill命令

如果一个指令的执行时间太长,或是屏幕上的输出太多,可以按下ctrl-c终止执行。但是只对在前台执行的进程有效,后台执行的进程不会接收键盘的按键输入,这时可以用kill命令杀死进程。

例如:kill -9 进程PID

一般先用ps命令列出各进程的pid,然后用kill命令杀死指定的进程。

kill命令还可以向进程发送信号

kill –l 可以显示kill命令所能发送的信号种类。 kill 命令的一般格式:

kill [参数] 进程1,进程2?..

例如:kill -9 835(值为9的信号表示强制中断并杀死进程)

实验测试命令

1. 先用ps命令列出当前的进程信息。ps 2. 后台执行locate lib |less & 3. ps

4. kill -9 [locate进程的pid] 5. kill -9 [less进程的pid] 6. ps

7. kill -9 [shell进程的pid]

第2章 基于Linux的程序设计基础

大多数Linux软件是经过自由软件基金会(Free Software Foundation)提供的GNU公开认证授权的,因而被称为GNU软件。GNU软件免费提供给用户使用,并被证明是可靠和高效的。许多流行的Linux 实用程序如C编译器、SHELL和编辑器都是GNU软件应用程序。

在Linux操作系统下进行C程序开发的标准主要有两个:ANSI C 标准和POSIX标准。ANSI C 标准是ANSI(美国国家标准局)于1989年制订的C 语言标准,后来

15

被ISO接受为标准,因此也称ISO C。

POSIX标准是最初由IEEE开发的标准族,部分已被ISO接受为国际标准。

2.1 关于编辑器

1.VIM编辑器 VIM简介

VI(Visual Interface)是Linux世界最常用的全屏编辑器,所有的Linux

都提供该编辑器,它在Linux中的地位如同EDIT在DOS中的地位。VIM是VI的加强版,与VI完全兼容。

具体使用可查找资料,通过上机练习掌握。 2.emacs编辑器

emacs编辑器是由Richard Stallman发明的,他创立了FSF(自由软件基金会)。Emacs是 Linux下非常强大的编辑工具,详细使用可参看emacs的在线手册。

3.图形用户界面下的文本编辑器gedit 建议编程时使用该编辑器。

2.2 关于编译器 GNU CC(GCC)

GNU CC(通常称为GCC)是GNU项目的编译器套件。它能够编译C、C++和Objective C语言编写的程序。GCC能支持多种不同的C语言变体,比如ANSI C和传统(Kernighan和Ritchie, K&R)C。

GCC的编译过程分为四个阶段:预处理,编译,汇编,链接。 例如:C 源程序的编译

gcc –o hello hello.c

若程序中用到数学函数,必须和函数库连接,除了在源文件中加入 #include 外,在编译时加上 –lm参数。 在linux字符命令下运行程序文件:./文件名。 例如: ./ hello

16

2.3 关于调试工具 GDB

Linux系统中包含了GNU调试程序,它是一个用来调试C和C++程序的调试器,可以使程序开发者在程序运行时观察程序的内部结构和内存的使用情况,功能:

运行程序,设置所有能影响程序运行的参数和环境。 控制程序在指定的条件下停止运行。 修改程序的错误,并重新运行程序。 动态监视程序中变量的值。

可以单步执行代码,观察程序的运行状态。

关于GDB的详细内容和使用请查找资料,通过上机练习掌握。 2.4 库函数和系统调用

1.库函数和系统调用

库函数一般完成常见的特定功能,通常由某一个组织制作发布,并形成一定的标准,可以应用于不同的平台而不需要做任何修改。例如C函数库能够被大多数C编译器支持。

系统调用一般与操作系统相关,不同的操作系统使用的系统调用可能不同。如图所示为Linux函数库调用与系统调用示意图:

17

应用程序 库函数 系统调用函数 用户模式 系统调用接口 内核模式 Linux内核 系统资源

库函数在实现时也可能需要使用系统调用,但它封装了系统调用的部分操作,用户不必关心它使用了哪些系统调用。

2.glibc函数库

GNU的C 函数库glibc是Linux最重要的函数库,它定义了ISO C 标准指定的所有库函数,以及由POSIX或其它UNIX操作系统变种指定的附加特色,还包括与GNU系统相关的扩展。

3.系统调用

系统调用是操作系统提供给外部程序的接口。在C语言中,操作系统的系统调用通常通过函数调用的形式完成。在Linux系统中,系统调用函数定义在glibc中。

4.库文件的使用与创建

在进行程序开发时,几乎都会使用printf()这样的函数完成特定的功能,但程序员自己并没有实现这些代码,这是因为库文件的支持。程序员在连接自己的应用程序时,编译器会查找到对应函数的连接位置,而在运行时,该程序将在当前系统内存空间中查找该函数在对应库文件中的位置(显然操作系统需要加载该库文件)

库函数是由系统提供的供开发人员开发时调用的完成特定功能的函数,库文件由软件提供商的库函数集合,一般就是.o的目标文件。

软件提供商在提供库文件时还提供了该库文件的头文件(.h)和简要说明手册。在进行软件开发时,一般都需要使用库文件,主要原因:

便于编程 对于部分经常使用的函数,C语言都提供了对应的库函数,便于开发。

18

隐藏具体的函数实现 程序员在使用库文件时,只需包含所需函数所在的头文件,加载目标库文件,而不必关心该函数的具体实现,从而降低开发难度和开发周期。

Linux环境下主要有两种库文件:

静态库。在Linux中,以.a为后缀,应用程序从静态库中直接复制函数到二进制映象文件。

共享库。在Linux中,libxxx.so.x.x 为命名格式。可执行文件在运行时将函数代码从共享库文件中读出,从而间接引用。

Linux系统中使用的系统库函数基本路径为: /lib; 系统必备共享库 /usr/lib; 标准共享库和静态库 /usr/local/lib; 本地函数库 系统默认的搜索路径是/lib和/usr/lib 2.5 Linux标准的I/O流

对于开发人员来说,计算机系统提供了两种接口:系统调用和库函数。一般情况下,无论是编写系统库还是应用程序,都离不开I/O这个重要环节。相对于低级的I/O操作(即系统调用级的I/O),标准I/O库函数处理了很多细节,如缓冲分配等。使用缓冲的好处之一是减少系统调用的开销,当然缓冲也会带来一些意想不到的结果,比如本来应该输出的内容却没有输出,或者在处理数据边界时产生数据丢失等问题。

考虑到程序代码的可移植性,开发人员在编写代码时尽可能使用标准库函数。头文件中包含了标准C的I/O库,标准C的I/O库在所有通用计算机上的C语言实现都是相同的。

系统级的I/O操作函数都是针对文件描述符的。即打开一个文件时会返回一个文件描述符,然后可以直接对文件描述符进行操作。

对于标准的I/O函数来说,打开或创建一个文件时,会返回一个指向FILE对象的指针。该FILE对象通常是一个结构体,它包含了输入输出函数库为管理该FILE对象所需要的尽可能多的信息,包括用于实际输入输出文件的文件描述符、指向流缓存的指针、缓存的长度、当前在缓存中的字符数以及出错标志等。

每一个进程在开始运行时都要先打开3个标准的文件指针,即标准输入stdin、标准输出stdout和标准错误stdout,除非特别改动(如使用重定向或管道技术),

19

这些文件指针指向与用户终端关联的流。

stdin(文件描述符为0:标准输入、默认输入keyboard), stdout(文件描述符为1:标准输出,默认输出monitor) and stderr(文件描述符为2:标准错误输出,默认输出monitor)。

1.标准的C库函数提供了标准的输入输出函数scanf()和printf(),这两个

函数使用标准的输入输出流stdin和stdout。

2.同时也提供了标准错误输出流stderr 。程序一般把一些警告和错误消息写

入stderr。

默认情况下,系统会对写入stdout的输出进行缓冲,这样,特定的消息可能不会在printf返回后立即出现;而写入stderr的消息不会被缓冲,所以调试程序时,应使用stderr。

如:fprintf(stderr, “ ”,??);

错误信息输出函数perror函数:向标准错误中输出一个对应于errno当前值的消息。

#include

Void perror(const char *s);

如果s不为空,perror就输出一个s指向的字符串,后面跟着一个冒号和一个空格,然后,perror输出一个对应于errno当前值的错误消息。后面跟着一个新行。

strerror函数:返回一个指向系统错误消息的指针,这个错误消息对应errno错误码。

举例:比较下面两段代码,指出输出的不同。 #include main() {

while(1) {

printf (\sleep (1);

20

}

}

#include main() { }

2.6 在线文档

在Linux系统下,常用的在线帮助文件有man、info以及HOW-TO等。 1.Man手册

Man 即manual,是Linux系统手册的电子版本,通常分为不同的小节。 Man 1:命令 可以查看普通用户命令使用介绍。 Man 2:系统调用 可以查看系统调用函数

Man 3:函数库调用 可以查看普通函数库中的函数 Man 4:特殊文件 可以查看/dev目录中的特殊文件

Man 5:文件格式和约定 可以查看/etc/passwd等文件的格式 Man 6:游戏 Man 7:杂项和约定 Man 8:系统管理命令 Man 9:内核例程

例如查看fork系统调用的用法:man 2 fork 第3章 进程管理

3.1 Linux 程序存储结构与进程结构

1. Linux 可执行文件结构 while(1) { }

fprintf (stderr,\sleep (1);

21

假设 Linux文件系统当前目录下,有一个可执行的程序文件test。命令file test 可列出此文件的基本情况。

#file test

Test:ELF 32-bit LSB executable,Intel 80386,version 1(SYSV),for GNU/linux 2.2.5,dynamically linked (uses shared libs),not stripped.

用size命令可列出二进制可执行文件结构情况。 #size test

//代码区 静态数据/全局初始化数据区 未初始化数据区 十进制总和 十六进制总和 文件名 Text 906 Test

由此可见,可执行文件在存储时(没有调入内存),分为代码区(text)、数据区(data)、未初始化数据区(bss)。

(1) 代码区(text) 存放CPU执行的机器指令,通常代码区时刻共享

的和只读的。

(2) 静态数据/全局初始化数据区(data) 简称数据段,该区包含了在

程序中明确被初始化的全局变量,已经初始化的静态变量(包括全局静态变量和局部静态变量)和常数数据。

例如:一个不在任何函数内的声明(全局数据)如下: int maxcount=99; //存储在数据区 在任意位置定义的静态变量

static mincount=100;//也存储在数据区

(3) 未初始化数据区(bss)存入的是全局未初始化的变量和未初始化的

静态变量。BSS区的数据在程序开始执行之前被内核初始化为0或空指针NULL。例如一个不在任何函数内的声明未初始化变量: long sum[1000]

将变量sum存储到未初始化数据区。

data 284

bss dec hex 4

1194

4aa

filename

22

未初始化变量 (BSS区,用0初始化) 已初始化全局变量、静态变量和常量数据(data) 可执行代码 (代码区text) 可执行文件结构(用size查看)

2. Linux进程结构

程序加载以后,则将演变成一个或多个进程(多个进程的原因是进程在运行过程中可以再创建新的进程),一个进程是一个运行着的程序段,一个进程主要包括在内存中申请的空间,代码段,数据段,BSS,堆,栈以及内核进程信息task_struct,打开的文件,上下文信息以及挂起的信号等。

23

高端地址

低端地址

argc,argv ,环境 命令行参数和环境变量 函数调用的活动记录 栈 (返回地址、参数、已 保留的寄存器、自动变 量)

用malloc函 数族分配的堆 内存 未初始化的静态变量(BSS区,用0初始化) 已初始化的全局变量、静态变量和常量(数据区) 程序文本(代码区) 进程内存结构(未列出在内核申请的资源)

24

已初始化的静态变量是磁盘上可执行模块的一部分,而未初始化的静态变量则不是,在运行时才分配空间,并被赋予初值0。

栈区(stack) 由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放。

堆区(heap) 用于动态内存分配。一般由程序员分配和释放,若程序员不释放,程序结束时可能由OS回收。

栈和堆的区别:

栈是由编译器在程序运行时分配空间区域,由操作系统维护。里面的变量通常是局部变量、函数参数等。堆是由malloc()函数分配的内存块,内存释放是由程序员手动控制。 3.2 命令参数

命令行组成:命令标记 参数标记(可能多个) 例如:mine –c 10 2.0(共4个标记)

当用户输入一个对应于C的可执行程序的命令行时,命令解释程序就将命令行解释成标记,并将结果以参数数组的形式传送到程序中。

程序: int main(int argc ,int *argv[]); 参数argc包括命令行标记的个数,4

argv是一个指向命令行标记的指针数组。如:

argv[ ]

0 1 2 3 4 NULL ?m‘ ?i‘? n‘ ?e‘‘ ?\\0‘ ?-‘ ?c‘ ?\\0‘ ?1‘ ?0‘? ?\\0‘ ?2‘ ?.‘ ?0‘‘ ?\\0‘ 25

3.3 进程创建

当操作系统启动时,会创建系统所有进程的祖先进程init进程,进程ID号为1。其它所有进程都是该进程的子孙进程,通过fork系统调用创建。

每个进程有自己独立的运行空间。Linux系统在内核头文件include/linux/sched.h中定义了结构体 struct task_struct来管理每个进程的资源

每一个进程都有一个唯一的进程ID号。其数据类型为pid_t,在中定义。

Fork系统调用: #include #include < sys/types.h > pid_t fork(void)

功能:进程通过调用fork创建新的进程。调用者进程成为父进程(parent),被创建的进程称为子进程(child)。

pid_t:用来表示进程ID的无符号整数类型。

函数getpid()、getppid()分别返回进程ID和父进程ID。

fork函数拷贝了父进程的内存映象,这样,新进程就会收到父进程地址空间的一份拷贝。

两个进程在fork语句之后,都继续执行后面的指令(分别在它们自己的内存映象中执行),子进程的执行位置为fork返回位置。

fork函数返回值允许父进程和子进程区别自己并执行不同的代码。 fork函数向子进程返回0; 而向父进程返回子进程的ID。 当fork失败时,返回-1并设置errno。 例1:

#include #include int main () {

printf (“The process ID is %d\\n”, (int) getpid ());

26

printf (“The parent process ID is %d\\n”, (int) getppid ()); return 0; } 例2:

#include #include #include int main () {

pid_t child_pid;

printf (“the main program process ID is %d\\n”, (int) getpid ()); child_pid = fork (); if (child_pid != 0) {

printf (“this is the parent process, with id %d\\n”, (int) getpid ()); printf (“the child’s process ID is %d\\n”, (int) child_pid); } else

printf (“this is the child process, with id %d\\n”, (int) getpid ()); return 0; }

3.4 exec函数

Fork函数创建了调用进程的一份拷贝,但很多应用程序都需要子进程执行与其父进程不同的代码。exec函数族提供了用新的映象来覆盖调用进程的进程映象的功能。

fork-exec配合应用的传统方式是:子进程用exec执行新程序,而父进程继续执行原来的代码。

一般,wait、exec联合使用的模型为: int status;

............ if (fork( )= =0)

27

{

...........; execl(...); ...........; }

wait(&status);

有六种不同形式的exec函数,其主要区别在于命令行参数和环境变量的传递方式的不同以及是否要给出可执行文件的完整的路径名。

#include extern char **environ;

int execl(const char *path, const char *arg0,……/*,char *(0)*/);

int execle(const char *path, const char *arg0,……/*,char *(0),char *const envp[]*/); int execlp(const char *file, const char *arg0,……/*,char *(0)*/); int execv(const char *path, const char *argv[]);

int execve(const char *path, const char *argv[],char *const envp[]); int execvp(const char *file, const char *argv[]);

函数名中包含p的函数:execlp和execvp,接收一个程序文件名作参数,并在当前可执行程序目录下查找该文件。名字中不带p 的函数必须要提供可执行文件的全名(包括路径)。

函数名中包含v的函数:execv,execve,execvp,接收一个以NULL结束的指向字符串的指针数组的参数,作为启动新的程序的参数。(const char *argv[])

函数名中包含l的函数:execl、execle、execlp,接收一个使用C语言的可变参数机制的参数列表。

函数名中包含e的函数:execle、execve,接收一个环境变量数组参数,这个参数必须是一个以NULL结束的指向字符串的指针数组,每个字符串的形式必须是:环境变量名=值。

因为exec是用另一个程序替换调用程序,除非执行失败,否则不会返回。

举例:程序3_1.c #include

#include #include #include

28

/* Spawn a child process running a new program. PROGRAM is the name of the program to run; the path will be searched for this program. ARG_LIST is a NULL-terminated list of character strings to be passed as the program‘s argument list. Returns the process ID of the spawned process. */

int spawn (char* program, char** arg_list) {

pid_t child_pid;

/* Duplicate this process. */ child_pid = fork (); if (child_pid != 0)

/* This is the parent process. */ return child_pid; else {

/* Now execute PROGRAM, searching for it in the path. */ execvp (program, arg_list);

/* The execvp function returns only if an error occurs. */ fprintf (stderr, \ abort (); } }

int main () {

/* The argument list to pass to the ―ls‖ command. */ char* arg_list[] = { \ \ \ NULL /* The argument list must end with a NULL. */ };

/* Spawn a child process running the ―ls‖ command. Ignore the returned child process ID. */ spawn (\

printf (\ return 0; }

29

3.5 等待进程结束以及wait系统调用

观察程序3_1.c的运行结果,会发现ls命令的运行结果有时会出现在“done with main program ”之后。 这是因为运行ls命令的子进程和父进程并发执行,且具有异步性,无法预知它们的执行顺序。

但有时我们需要让父进程等待子进程结束后再运行,这时可以使用wait系统调用。

#include pid_t wait(int *stat_loc);

pid_t waitpid(pid_t pid, int *stat_loc, int options)

父进程可以执行wait或waitpid一直阻塞到子进程结束。

Waitpid允许父进程等待一个特定的子进程,这个函数还允许父进程非阻塞地检查子进程是否已经终止了。

函数的返回值: (1) (2) (3)

如果wait或waitpid是因为子进程的状态被报告而返回(如子进程退出),函数返回那个子进程的进程ID号。 如果出现错误,函数返回-1,并设置errno。

如果waitpid中用选项WNOHANG调用,函数以非阻塞的方式运行,若检测到有结束的子进程,函数清除它,然后返回子进程的ID,若没有检测到有结束的子进程,函数简单的返回0,并不阻塞等待。

参数: 1.状态值

函数通过一个指向整型变量的指针返回子进程的状态代码(int *stat_loc)。若stat_loc不为NULL,wait或waitpid函数就将子进程的状态信息在这个单元中。

POSIX为测试子进程的返回状态指定了六个宏,每个宏都将子进程返回给父进程的状态值作为参数。

WIFEXITED(int stat_val); WEXITSTATUS(int stat_val);

当子进程正常终止时,WIFEXITED(int stat_val)求出的值为非0,则WEXITSTATUS(int stat_val)求出的就是子进程的返回值。

30

WIFSIGNALED(int stat_val); WTERMSIG(int stat_val);

当子进程由于一个未捕捉的信号而终止时,WIFSIGNALED(int stat_val)求出的值为非0,则WTERMSIG(int stat_val)求出的就是那个信号的编号。

WIFSTOPPED(int stat_val); WSTOPSIG(int stat_val);

如果当前有子进程被停止了,则WIFSTOPPED(int stat_val)求出的值为非0,而WSTOPSIG(int stat_val)求出的就是使子进程停止的那个信号的编号。

例如:将3_1.c程序的main函数改写一下:

int main () {

int child_status;

/* The argument list to pass to the \char* arg_list[] = {

\ \ \

NULL /* The argument list must end with a NULL. */ };

/* Spawn a child process running the \command. Ignore thereturned child process ID. */

spawn (\

/* Wait for the child process to complete. */ wait (&child_status);

if (WIFEXITED (child_status))

printf (\ WEXITSTATUS (child_status)); else

printf (\return 0; }

31

3.6 僵死进程(Zombie Process)

问题:如果进程终止了,但它的父进程没有等待它,会发生什么情况? 答案: 一个进程终止,该进程变成了一个僵进程(zombie)。僵进程一直停留在系统中,直到有进程等待它们为止。如果父进程没有等待子进程就终止了,子进程就成了孤儿进程(orphan),由一个特殊的进程收养,这个进程是init进程,PID 为1。init进程周期性地等待子进程,所以成为孤儿的僵进程都被删除了。

3.7 如何异步地清除子进程

在fork-exec模式中,父进程创建子进程,子进程转去执行另一段程序代码,父进程调用wait阻塞等待子进程的结束。但有时父进程在创建一个或几个子进程后,并不阻塞,而是和子进程并发执行,这时,父进程如何清除退出的子进程呢?

一种比较好的解决方案是利用信号机制。

当一个子进程退出时,Linux操作系统会自动地向其父进程传递信号SIGCHLD,在缺省情况下,父进程接到这个信号不做任何事情,我们可以通过自己定义父进程的SIGCHLD信号处理程序来清除子进程。

例如:

#include

#include #include #include

sig_atomic_t child_exit_status; // sig_atomic_t是一个小到可以实现原子访问的整数类型

void clean_up_child_process (int signal_number) {

/* Clean up the child process. */ int status;

wait (&status);

/* Store its exit status in a global variable. */ child_exit_status = status; }

int main () {

32

/* Handle SIGCHLD by calling clean_up_child_process. */ struct sigaction sigchld_action;

memset (&sigchld_action, 0, sizeof (sigchld_action)); sigchld_action.sa_handler = &clean_up_child_process; sigaction (SIGCHLD, &sigchld_action, NULL);

/* Now do things, including forking a child process. */ /* ... */ return 0; }

关于信号的详细信息,参考第4章。 第4章 信号 4.1 什么是信号?

信号在Unix或系统Linux中用作通知进程某个特定的事件已经发生,是系统中实现进程之间的通信以及巧妙地操纵进程的一种机制。

一个信号就是发送给某个进程的一种特殊的消息。信号是异步的。当一个进程收到一个信号时,它可以暂停正在运行的代码,转去执行信号处理程序。

可把信号理解为windows下的事件。

系统有很多具有不同含义的信号,每个信号都有一个以SIG开头的符号名。信号的名字都定义在头文件signal.h中。信号的名字表示大于0的小整数。

其中有两个信号是提供给用户使用的,SIGUSR1,SIGUSR2,没有预先指定的用途。其它信号都由系统预先指定了用途,并有它们的默认行为。

对于每一个信号,当信号发生时,都有一个缺省的处理,当进程收到一个信号时,若程序没有定义信号处理,则进程对信号执行缺省的处置,这是当处理信号时由内核运行的,对大多数信号类型,程序可以定义特定的信号处理程序,或忽略信号。

Linux操作系统在特定的条件下会向进程发送特定的信号。如当进程试图执行非法操作时,操作系统可能会向进程发送SIGBUS (bus error), SIGSEGV (segmentation violation), and SIGFPE (floatingpoint exception)信号。

一个进程也可以向另一个进程发送信号。

例如:一个进程通过向另一进程发送SIGTERM or SIGKILL 信号而结束该进程。或者一个进程向另一进程发送命令,这时就会用到SIGUSR1,SIGUSR2信号。

33

4.2设置与特定信号相关的动作

sigaction函数允许调用程序检查或指定与特定信号相关的动作。

#include

int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);

成功:返回0;

失败:返回-1,并设置errno。 struct sigaction结构: struct sigaction { */

} 参数:

int sig :信号编号

const struct sigaction *restrict act :一个指向struct sigaction结构的指针,用来说明要采取的动作。

struct sigaction *restrict oact:也是一个指向struct sigaction结构的指针,负责接收与信号相关的前一个动作。。

如果act为NULL,对sigaction的调用就不能改变与信号相关的动作;若oact为NULL,对sigaction的调用就不会返回与信号相关的前一个动作。

在程序运行过程中,当按下ctrl+c时,进程会收到一个SIGINT信号,其缺省的处理动作是终止程序的运行。当进程在运行过程中调用abort()系统调用时,会向自己发送一个SIGABRT信号,其缺省的处理动作也是终止程序的运行。进程也可以利用kill系统调用向另一个进程发送信号。kill(pid_t pid,int sig)

例1:下面的程序在运行过程中,用户按下ctrl+c键时,进程显示捕捉到信号

sigset_t sa_mask; //处理程序执行过程中需要阻塞的额外信号。 int sa_flags; //特殊标志符或选项

void (*sa_sigaction)(int,siginfo_t *,void *); //实时处理程序

void (*sa_handler)(int); /*SIG_DFL、SIG_IGN或指向函数的指针。SIG_DFL

说明sigaction应执行信号的默认行为;SIG_IGN说明进程应忽略信号,丢弃它们。

34

的信息并退出。

#include #include #include #include #include

void handler (int signal_number) {

printf(\ exit(0); }

int main ()

{ struct sigaction sa;

memset (&sa,0, sizeof (sa)); sa.sa_handler = &handler;

sigaction (SIGINT, &sa, NULL); while(1); }

例2:下面这段程序利用统计sigaction函数统计进程在运行过程中收到的信号SIGUSR1的次数。

#include #include #include #include #include

sig_atomic_t sigusr1_count = 0; void handler (int signal_number) {

++sigusr1_count;

printf (―SIGUSR1 was raised %d times\\n‖, sigusr1_count); }

int main () {

struct sigaction sa;

35

memset (&sa, 0, sizeof (sa)); sa.sa_handler = &handler;

sigaction (SIGUSR1, &sa, NULL); while(1); return 0; }

编译运行该程序。另外打开一个终端窗口,运行ps命令,查看4_1进程的PID号,用kill命令向4_1进程多次发送SIGUSR1信号,最后发送KILL信号结束进程。 第5章 进程间通信 5.1 管道通信

最简单的linux进程间的通信机制是管道,管道由特殊文件来表示。

在Linux SHELL中,符号|可以创建管道,例如ls | less,详见第一章 管道命令一节。

1.无名管道 具有的特点:

a) 半双工:数据只能在一个方向上流动。 b) 只能在具有家族关系的进程中使用。

c) 虽然称做文件,但不属于文件系统,只存在于内存中。 d) 从一端写入,从另一端读出。 e) 没有名字

f) 管道的缓冲区是有限的,在管道创建时,为其分配一个页面大小。 g) 写入管道的数据,读完之后就从管道中消失。

创建管道的函数:#include int pipe(int fildes[2]);

该函数创建了一个通信缓冲区,程序可以通过文件描述符fildes[0]和fildes[1]来访问这个缓冲区。写入管道的数据可以按先进先出的顺序从管道中读出。

返回值:0:成功

-1:失败并设置errno。 例如: int fd[2]

pipe(fd);

36

文件描述符:fd[0],fd[1]

写操作:指针fd[1] 例如: char buf1[]=‖hello‖ write(fd[1],buf1,sizeof(buf1)) 读操作:读指针fd[0] 例如: char buf2[]; int len; len=read(fd[0],buf2,100); len:正常情况下为读出的字符串长度,若对空管道进行read调用,返回0 例如:进程在运行中创建子进程,父进程利用管道向子进程发送信息,子进程从管道读出信息,并显示。

#include #include #include int main () {

int fds[2],len; pid_t pid;

static char ss[]=\ char output[255];

/* Create a pipe. File descriptors for the two ends of the pipe are placed in fds. */ pipe (fds);

/* Fork a child process. */ pid = fork (); if (pid == 0) {

37

*/

/* This is the child process. Close our copy of the write end of the file descriptor.

close (fds[1]); len=read (fds[0],output,255); close (fds[0]); fprintf(stderr,\ } else {

/* This is the parent process. */ /* Close our copy of the read end of the file descriptor. */ close (fds[0]); write(fds[1],ss,sizeof(ss)); close (fds[1]); wait(NULL); } return 0; }

2.有名管道(FIFO):

命名管道克服了无名管道的限制。略。

5.2 消息队列

消息队列(Message Queue)允许一个或多个进程向它写入与读取消息,从而实现进程之间消息传递。

消息队列是消息的链表,存放在内核中并由消息队列标识符标识,该标识符又称为消息队列ID。

目前主要有两种类型的消息队列:POSIX消息队列以及系统V消息队列,系统V消息队列目前被大量使用。考虑到程序的可移植性,新开发的应用程序应尽量使用POSIX消息队列。

38

1. 系统V消息队列

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

消息队列的数据结构定义在sys/msg.h中。 struct msqid_ds { }

ipc_perm的结构:

struct ipc_perm {

uid_t uid; //ower‘s user id gid_t gid; //ower‘s group id uid_t cuid; //creater‘s user id gid_t cgid; //creater‘s group id

mode_t mode; //read write permissions ulong_t seq; //slot usage sequence number key_t key; //IPC key }

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

内核中的消息队列结构:

struct ipc_perm msg_perm; //读写权限

struct msg *msg_first; //指向队列中第一个消息的指针 struct msg *msg_last; //指向队列中最后一个消息的指针 msglen_t msg_cbytes; //队列中当前的字节数 msgqnum_t msg_qnum; //队列中当前的消息数 msglen_t msg_qbytes; //队列中允许的最大字节数 pid_t msg_lspid; //上一次发送消息的进程ID pid_t msg_lrpid; //上一次接收消息的进程ID time_t msg_stime; //上一次发送消息的时间 time_t msg_rstime; //上一次接收消息的时间 time_t msg_ctime; //上一次修改队列信息的时间

39

msqid

ipc_perm{} next type=100 length=1 data next type=200 Length=2 data NULL Type=300 Length=3 data Msg_first msg_last msg_ctime 消息队列中有三个消息节点。 对消息队列的操作有下面三种类型: (1) 打开或创建消息队列

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

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

(2) 读写操作

消息读写操作非常简单,对开发人员来说,每个消息都类似如下的数据结构: struct msgbuf{ long mtype; char mtext[1]; }; mtype成员代表消息类型,从消息队列中读取消息的一个重要依据就是消息的类型;mtext是消息内容,当然长度不一定为1。因此,对于发送消息来说,首先预

40

置一个msgbuf缓冲区并写入消息类型和内容,调用相应的发送函数即可;对读取消息来说,首先分配这样一个msgbuf缓冲区,然后把消息读入该缓冲区即可。

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

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

结构msg_queue用来描述消息队列头,存在于系统空间:

struct msg_queue { struct kern_ipc_perm q_perm; time_t q_stime; /* last msgsnd time */ time_t q_rtime; /* last msgrcv time */ time_t q_ctime; /* last change time */ unsigned long q_cbytes; /* current number of bytes on queue */ unsigned long q_qnum; /* number of messages in queue */ unsigned long q_qbytes; /* max number of bytes on queue */ pid_t q_lspid; /* pid of last msgsnd */ pid_t q_lrpid; /* last receive pid */ struct list_head q_messages; struct list_head q_receivers; struct list_head q_senders; }; 结构msqid_ds用来设置或返回消息队列的信息,存在于用户空间;

struct msqid_ds { struct ipc_perm msg_perm; struct msg *msg_first; /* first message on queue,unused */ struct msg *msg_last; /* last message in queue,unused */ __kernel_time_t msg_stime; /* last msgsnd time */ __kernel_time_t msg_rtime; /* last msgrcv time */ __kernel_time_t msg_ctime; /* last change time */ unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */ unsigned long msg_lqbytes; /* ditto */ unsigned short msg_cbytes; /* current number of bytes on queue */ unsigned short msg_qnum; /* number of messages in queue */ unsigned short msg_qbytes; /* max number of bytes on queue */ 41

__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */ __kernel_ipc_pid_t msg_lrpid; /* last receive pid */ }; //可以看出上述两个结构很相似。

有关消息队列的API函数

创建一个消息队列的顺序:

char *pathname int id ftok() key_t key msgget() msgsnd() int identifier msgrcv() msgctl() Key of IPC_PRIVATE

当创建一个消息队列时,需传递给msgget函数一个键值,类型是key_t,通常是至少32位的整数,这个键值通常由ftok函数生成。

或者是在创建消息队列时,传递给msgget函数一个特殊的键值IPC_PRIVATE。 1)文件名到键值

头文件中定义了key_t数据类型 #include #include key_t ftok (const char *pathname, int id); 函数根据pathname和id的低8位值,生成一个键值。返回与路径pathname相对应的一个键值。该函数不直接对消息队列操作,但在msgget()来获得消息队列描述字前,往往要调用该函数。

42

2)msgget函数

功能:新建或获取一个已经存在的消息队列的访问。

#include #include #include

int msgget(key_t key , int oflag);

参数key是一个键值,由ftok获得或者是IPC_PRIVATE;oflag参数是一些标志位。该调用返回与健值key相对应的消息队列描述字。该描述字将被其它的消息队列函数使用。

参数oflag可以为以下:IPC_CREAT、IPC_EXCL。

将参数oflag设置为IPC_CREAT,表示若没有与键值key相对应的消息队列存在,则创建队列,若队列已经存在,则返回队列的描述字。

若同时设置IPC_CREAT和IPC_EXCL,表示若没有与键值key相对应的消息队列存在,则创建队列,若队列已经存在,则返回一个EEXIST错误。 Oflag参数 No special flags IPC_CREAT IPC_CREAT| IPC_EXECL

Key does not exist Error,errno=ENOENT OK,create new entry OK,create new entry Key already exists OK,references existing object OK,references existing object Error,errno=EEXIST 在以下两种情况下,该调用将创建一个新的消息队列:

如果没有消息队列与健值key相对应,并且oflag中包含了IPC_CREAT标志位; key参数为IPC_PRIVATE;

调用返回:成功返回消息队列描述字,否则返回-1。

注:参数key设置成常数IPC_PRIVATE并不意味着其他进程不能访问该消息队列,只意味着即将创建新的消息队列。

当一个消息队列创建成功后,msqid_ds结构被初始化。 3)发送消息 消息数据结构:

43

中有消息模板数据结构的定义 struct msgbuf {

long mtype; //消息的类型,必须>0

char mtext[1]; //消息数据,,消息的长度可根据需要定义,不一定是1。 }

大多数应用可以根据需要定义自己的消息数据结构,不必局于模板的定义。 例如:

#defime MY_DATA 8

typedef struct my_msgbuf { long mtype; int16_t mshort;

char mchar[MY_DATA]; }Message;

发送消息的系统调用:

#include

int msgsnd(int msqid, const void *ptr, size_t length , int flag);

向msgid代表的消息队列发送一个消息,即将发送的消息存储在ptr指向的msgbuf结构中,消息的大小由length指定。

对发送消息来说,有意义的flag标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待。

造成msgsnd()等待的条件有两种:

当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量; 系统中的消息数太多。

若上述两个条件之一满足,且定义了flag标志为 IPC_NOWAIT,调用返回一个错误EAGAIN,否则,阻塞进程,直到阻塞被解除。

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

? 不满足上述两个条件,即消息队列中有容纳该消息的空间; ? msqid代表的消息队列被删除; ? 调用msgsnd()的进程被信号中断; 调用返回:成功返回0,否则返回-1。

4)接收消息

44

#include

ssize_t msgrcv(int msqid, void *ptr, size_t length, long type, int flag);

该系统调用从msgid代表的消息队列中读取一个消息,并把消息存储在ptr指向的msgbuf结构中。

msqid为消息队列描述字;消息返回后存储在ptr指向的地址,length指定msgbuf的数据成员的长度(即消息内容的长度),type为请求读取的消息类型,若type=0,返回队列中的第一个消息,若大于0,则返回队列中第一个type值等于该值的消息,若小于0,返回最小的type值小于或等于该值绝对值第一个消息。

读消息标志flag,定义了当一个请求消息类型不在队列中时,应该怎样处理。可以为以下几个常值的或:

IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,此时,errno=ENOMSG ,否则阻塞进程,直到解除阻塞。

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

? 消息队列中有了满足条件的消息; ? msqid代表的消息队列被删除; ? 调用msgrcv()的进程被信号中断;

MSG_NOERROR 如果队列中满足条件的消息内容大于所请求的length字节,则把该消息截断,截断部分将丢失。否则,将返回错误E2BUG。

调用返回:成功返回读出消息的实际字节数,否则返回-1。

5)msgctl函数

该函数提供了对消息队列的各种控制操作。 #include

int msgctl(int msqid, int cmd, struct msqid_ds *buff);

该系统调用对由msqid标识的消息队列执行cmd操作,共有三种cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。

IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buff指向的msqid_ds结构中;

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

IPC_RMID:删除msqid标识的消息队列;对于这个命令选项,第三个参数忽

45

略(NULL)。

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

(1) 创建消息队列createq.c。 (2) 向队列中发送消息writeq.c (3) 从队列中接收消息readq.c (4) 删除队列removeq.c

createq.c

#include #include #include #include

int main(int argc,char *argv[]) { key_t key; if (argc!=2) {

printf(\ return -1; }

key=ftok(argv[1],0);

printf(\

//msgget(key,IPC_CREAT|IPC_EXCL|00666);

if (msgget(key,0666|IPC_CREAT|IPC_EXCL)==-1) printf(\ printf(\ return 0; }

writeq.c #include #include #include #include #include #include

46

int main(int argc,char *argv[]) { struct msgbuf { long type;

char data[100]; };

key_t key;

struct msgbuf *ptr; int myqid;

char ss[]=\ if (argc!=2) {

printf(\ return -1; }

key=ftok(argv[1],0); myqid=msgget(key,0666);

ptr=calloc(sizeof(long)+sizeof(ss),sizeof(char)); ptr->type=1;

strcpy(ptr->data,ss);

msgsnd(myqid,ptr,sizeof(ss),0);

printf(\ return 0; }

readq.c #include #include #include #include #include #include

#define MAXMSG (8192+sizeof(long)) int main(int argc,char *argv[]) { struct msgbuf { long type;

char data[100]; };

key_t key;

struct msgbuf *ptr;

47

}

int myqid; ssize_t n;

if (argc!=2) {

printf(\ return -1; }

key=ftok(argv[1],0); myqid=msgget(key,0666); ptr=malloc(MAXMSG);

n=msgrcv(myqid,ptr,MAXMSG,1,0);

printf(\return 0;

removeq.c

#include #include #include #include #include

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

key_t key;

int myqid;

if (argc!=2) {

printf(\ return -1; }

key=ftok(argv[1],0); myqid=msgget(key,0666);

if (msgctl(myqid,IPC_RMID,0)==0 )

printf(\

48

return 0; }

命令:ipcs –q 可以显示队列的相关信息。

2. 略。

练习:设计两个进程通过消息队列进行通信的程序。一个进程产生一个-100到100之间的随机数,将其写入队列,另一进程从队列中读出,并显示出来。

提示:产生-100到100之间的随机数

#include #include

int random_range (unsigned const low, unsigned const high) {

unsigned const range = high - low + 1;

return low + (int) (((double) range) * rand () / (RAND_MAX + 1.0)); }

Int main() {

int i; ……..

srand(time(NULL));

i=random_range(-100,100); …….. …….. }

5.3 共享内存的通信(Shared Memory)

共享内存是最快的一种进程间通信方式。

两个不同进程A、B共享内存的意思是:同一块物理内存被映射到进程A、B各自的地址空间中。进程A可以即时看到进程B对共享内存中数据的更新情况,反

Posix消息队列

49

之亦然。

共享内存为内存块方式的数据段,其数据长度可为系统参数限制内的任意长度。在进程取得共享内存标识符并将共享内存与进程数据段连接后,就可对它进行读写操作,在所有的读写操作结束之后要解除共享内存和进程数据段的联系。

由于内核并不同步进程对共享内存的读写,所以在编程时要使用信号量机制同步进程对共享内存的读写访问。

共享内存处理函数: 1.Shmat函数

创建一个共享段:shmget( key, size, flags )

用于创建一个与key相对应的共享内存段。若创建成功,返回共享内存段的标识符。失败返回-1。

#include

#include #include

int shmget( key, size, flags ) 参数说明: (1) key

integer数据类型,用来定义要创建的共享段。不同的进程通过相同的key值可以访问同一个共享段。

有两种方式:①参数key等于IPC_PRIVATE,系统自动创建一个全新的共享段。②参数key为一integer数,且该key值没有与之关联的共享段标识符。 (2) size

第二个参数定义了共享段的字节数。一般是页大小的整数倍。 (3) flags

定义shmget的选项。Flags是几个flag值的or运算。flag取值包括: IPC_CREAT:该取值表示允许新建一个与key值对应的共享段或一个全新的共享段(key=IPC_PRIVATE)。

IPC_EXCL:总是与IPC_CREAT一同使用。若设置了该选项值,当shmget时,若已经存在了与key对应的共享段,则调用失败,返回-1。若没有设置该选项值,当shmget时,若已经存在了与key对应的共享段,则返回已经存在的共享段的标识符。

模式falgs:定义用户主、同组用户以及其它用户对该共享段的读写权限。例如0666定义了各种用户对共享段均有读写权。或者用在

50

本文来源:https://www.bwwdw.com/article/ytzx.html

Top