UNIX 编程资料
第一章 概述
1.1UNIX的版本
UNIX操作系统是贝尔实验室于六十年代末用C语言研制开发的。经过几十年的发展,已经成为流行于从大型机、小型机到工作站甚至微机等多种平台的操作系统。UNIX的成功同时也推动了C语言的普及。本教材的目的是讲解UNIX系统下的C程序设计,使C程序员快速掌握UNIX系统下的编程开发。作者在进行UNIX编程开发的实践过程中,深感实例的重要性-一个简短的C语言实例往往胜过长篇累牍的文字说明,当然了,文字说明也是必不可少的。本教材将本着实例优先的原则,使您能够对UNIX编程开发快速入门。
UNIX的版本不统一是出了名的,从UNIX的发展历史来看,主要有两大流派:AT&T的UNIX系统V版本和加州大学伯克利分校的BSD版本,在此基础上,各家UNIX厂商均开发了各自的UNIX操作系统。如:工作站厂商中有HP的hpux、SUN的solaris、SGI的irix、IBM的AIX等,小型机有VAX上的Ultrix,微机上有SCO
UNIX、微软的Xenix以及随着Internet而风靡全球的Linux等。由于Windows
NT的异军突起,对UNIX的市场形成巨大的威胁,各大UNIX厂商不得不联合起来,在工作站市场上,统一以系统V版作为标准,加入BSD版本中的一些优点,支持统一的CDE(Common
Desktop Environment)窗口环境,以与Windows NT进行对抗。
1.2 UNIX编程环境
UNIX操作系统通过Shell程序实现系统与用户的交互,在Shell提示符下,用户键入UNIX命令,即可得到操作系统的输出结果。BSD系统的常用Shell是C Shell,缺省提示符是"%",系统V的常用Shell是Bourne Shell(现在多为Korn
Shell),缺省提示符是"$",有关Shell的编程,我们在后面的章节中进行介绍。
UNIX上的标准编译器是cc。在Shell提示符下(以C Shell为例)键入下列命令:
%cc -o hello hello.c
即将C文件hello.c编译为可执行文件hello。在编译多个文件生成一个可执行文件时,UNIX提供命令make。用户需要针对多个C文件,按照一定的格式编写一个叫做Makefile的文本文件。下面是SGI上的一个Makefile的例子:
CC = cc
CFLAGS = $(DEBUG) -cckr -I$(INC)/X11 -DSYSV
DEBUG = -g
INC = /usr/include
LDFLAGS = -lXext -lXm -lXt -lX11 -lPW -lc
OBJS = initx.o windowx.o
TGTS = showxwin
all:: $(TGTS)
showxwin: $(OBJS)
$(CC) -o $@ $(OBJS) $(CFLAGS) $(LDFLAGS)
大写字母的字串是一些宏,CC是编译器的名字、CFLAGS定义cc的编译开关、DEBUG是调试宏、INC是头文件所在目录、LDFLAGS定义了编译连接库、OBJ定义了目标文件名、TGTS定义了可执行文件名。在Shell提示符下直接键入:
%make
即可将Makefile中指定的所有C文件进行编译并生成可执行文件。
1.3 UNIX编程中的基本概念
在讨论UNIX编程开发前,首先需要阐明系统调用和库函数这两个概念。
一个系统调用指一个需要操作系统代表用户程序来执行某些任务的请求。例如:read是一个系统调用,它请求操作系统存储在一个磁盘设备(或其他设备)上的数据去填充一个缓冲区。如果任何人在他们想执行任务的时候都能随便访问设备,那么后果将是不可预测的。所以,这种服务必须请求操作系统来做,它(经常是透明地)记录所有处理每个设备的请求。
而一个库函数,并不经常需要操作系统来执行其任务。例如数学库函数中的sin(),cos()等,这些计算只需要简单地对一个有限序列求和,所以并不需要操作系统干预。
在UNIX操作系统中,有一个常用的命令man,可用来查阅命令、库函数和系统调用等的具体使用方法。传统 Unix 联机帮助手册的分节法为:
1 用户级命令(User-level commands)
2 系统调用(System calls)
3 库函数(Library functions)
4 设备及驱动程序(Devices and device drivers)
5 文件格式(File formats)
6 游戏(Games)
7 杂项(Various miscellaneous stuff - macro packages etc.)
8 系统维护及操作命令(System maintenance and operation commands)
第二章 标准输入/输出库
2.1 概述
本章介绍UNIX的标准输入/输出库,UNIX提供一些库函数完成高级输入/输出,为程序员提供了三方面的主要功能:
·自动开辟缓冲区。即使一次读或写的数据只有几个字节,库函数仍然在大到由数千个字节组成的"块"中执行实际输入或输出(缓冲区大小通常由头文件stdio.h中的常量BUFSIZ定义)。这个缓冲区在内部开辟给库函数使用,对于程序员来说是透明的;
·自动执行输入和输出转换。
·输入输出被自动格式化。以上两点在C语言的教程中一般均以讲到。
在标准输入/输出库中,一个文件被称为一串字符流,并且被一个指向类型为FILE的目标指针所描述,该指针被称为文件指针。在UNIX中文件指针stdin、stdout、stderr是预先定义好的,分别对应标准输入(键盘)、标准输出(终端屏幕)和标准错误输出。
2.2 库函数介绍
·文件创建和关闭
fopen()用于打开已存在的文件或创建新文件
·文件读写
1、 一次处理一个字符 getc(), putc()
2、 一次处理多个字符 fgets(), fputs()
3、 文件的二进制读写 fread(), fwrite()
4、 文件的格式化输入/输出 fscanf(), fprintf()
5、 字符串的格式化输入/输出 sscanf(), sprintf()
·文件移动定位
用于在文件中移动的标准输入/输出库函数是fseek(),它接收三个参数:一个文件指针指向一个打开的字符流;一个整数指明要移动的字节数,称为offset;一个整数指明从文件中什么位置移动。
第三章 低级输入/输出
3.1 概述
与第二章内容相对应,本章介绍UNIX系统中通过系统调用来实现的输入/输出,通常称之为低级输入/输出。这些系统调用能够直接实现对设备(如磁带驱动器等)的输入和输出,程序员能够决定要使用的缓冲区的大小,而不象标准输入/输出库函数那样透明设定缓冲区大小。
在标准输入/输出库中,一个文件是由一个文件指针来对应的。当使用低级界面时,则用一个文件描述字对应一个文件。文件描述字是一个小的整数。有3个事先定义的文件描述字0、1和2,分别对应标准输入、标准输出和标准错误输出。一般说来,文件描述字都是作为系统调用的第一个参数给出的。
3.2 相关系统调用介绍
·文件创建和关闭
open()用于为读写而打开一个文件,或用它来创建新文件。
int open (const char *path, int oflag, ... /* mode_t mode */);
open使用三个参数:一个字符串path包含要打开的文件名;一个整数oflag指明文件将被如何打开;整数mode在创建文件时使用。常用的oflag包括:
O_RDONLY 打开文件仅用于读。
O_WRONLY 打开文件仅用于写。
O_RDWR 打开文件用于读写。
O_CREAT 如果文件不存在,则创建,此时mode作为第三个参数给出。
close()用于关闭一个已经打开的文件。
·文件读写
read()用于读文件,格式为:
read(int fildes, void *buf, size_t nbyte);
三个参数说明如下:filedes是文件描述字;指针buf指向一个数据将被读入的缓冲区;整数nbytes指明要读的字节个数。成功时返回实际读入的字节数,出错则返回-1。
write()用于写文件,与read类似,格式为:
write(int fildes, void *buf, size_t nbyte);
三个参数说明如下:filedes是文件描述字;指针buf指向一个数据将被写入的缓冲区;整数nbytes指明要写的字节个数。成功时返回实际写入的字节数,出错则返回-1。
·文件移动定位
用于在文件中移动的低级输入/输出系统调用是lseek(),与fseek()类似,它也接收三个参数:一个文件描述字对应一个打开的文件;一个整数指明要移动的字节数,称为offset;一个整数指明从文件中什么位置移动。
·复制文件描述字
有时候有不只一个文件描述字对应一个文件。当创建子进程时(参加后面关于进程开发的章节),这一点很常用。为了获得一个新的文件描述字,并保证其与fd对应同一个文件,应调用
fd2 = dup(fd)
fd2现在和fd对应同一个文件,并且和fd一样在文件中有相同的位置。
第四章 文件与目录编程
4.1 基本概念
·文件目录概述
文件系统是UNIX对计算机技术的一大贡献!UNIX系统的文件管理十分灵活、功能强大,许多首次在UNIX系统中出现的概念被其他操作系统所采用,如MS-DOS等。
UNIX系统提供了一种层次目录方案。目录就象存放一组文件的柜子一样,目录也可以包括在其他目录中,这样就形成了一种庞大的、具有分支的组织方式,这种结构通常被称为树状结构。目录实际上也是一种特殊的文件。命令、数据文件、其他命令甚至设备(特别文件)都可以作为目录中的项(文件)。
·I标识号、I列表和I节点
一个目录是由一系列结构组成的;每个结构包含一个文件名和一个指向文件自身的指针,该指针是一个整数,称为文件的I标识号。当文件被访问时,它的I标识号用来作为索引打开一个系统表(I列表),系统中存放着文件(I节点)的实体。I节点包含了对文件的描述:
·文件自身的用户和用户组ID
·文件的保护码
·文件内容所在的物理磁盘地址
·文件的大小
·最后一次I节点改变的时间,最后一次使用和修改的时间
·连接该文件的次数,即它出现在其他目录中的次数
·一个指明文件类型的标记(目录、普通文件、特别文件)
·文件的三级保护
UNIX把使用文件的用户分成三个等级:文件所有者、同组用户和其他用户。文件所有者也称文件主,是文件的创建者,对该文件拥有所有权限;同组用户是具有相同组标识号的所有用户,文件主可以决定一个文件属于哪个组以及该组用户对文件的存取权;其他用户是指与文件主无关的用户,他们与文件主不属于同一个用户组,其他用户对一个文件的访问权限也由该文件主决定的。
一个文件的访问权限存放在该文件I节点的di_mode域中,di_mode的0-8位表示文件主、文件组用户和其他用户对该文件的存取权限。举个例子,用"ls
-l"命令可列出文件hello.c的模式和属性:
-rwxr-xr-x 1 yds user 58 9月 25日 10时54分 hello.c
最左面一栏显示了该文件的模式:文件主对该文件可读(r)、可写(w)、可执行(x),同组用户对该文件可读、可执行,其他用户对该文件可读、可执行。相应的,di_mode的0-8位为111101101(0755)。
4.2 文件编程介绍
·检查访问权限-access系统调用
access系统调用的格式为:
#include
int access(const char *path, int amode);
其中:参数path指出被检查文件的路径,参数amode指出访问权限。Access判断调用进程的实际用户对文件path是否具有amode所指定的访问权限,若有相应权限,access返回0,否则返回-1。
参数amode可取以下值或它们的逻辑"或":
R_OK 检查读权限
W_OK 检查写权限
X_OK 检查执行(搜索)权限
F_OK 检查文件是否存在
例如:access("hello.c", R_OK|W_OK),
用来检查实际用户对文件hello.c是否具有读/写权;access("hello.c",F_OK) 判断文件hello.c是否存在。
·链接与删除文件-link和unlink系统调用
link和unlink系统调用的格式为:
#include
int link (const char *path1, const char *path2);
int unlink(const char *path);
其中:参数path1指出已经存在的要被链接的文件路径名,path2指出要建立的链接文件。link实现path2到path1的链接,相当于给path1起了一个别名,同时文件path1的链接计数加1。若成功则返回0,否则返回-1。
参数path指出要被删除的文件路径名。Unlink删除由path指出的文件,若成功则返回0,否则返回-1。
·从I节点上获取信息-stat与fstat系统调用
stat与fstat的调用格式为:
#include
#include
int stat(const char *path, struct stat *buf);
int fstat(int fildes, struct stat *buf);
说明:stat和fstat都用于获取文件I节点中有关状态信息。stat根据参数path给出的文件路径名,搜索它对应的盘I节点,而fstat则根据参数fildes给出的文件描述字去查找对应的I节点。这两个调用都把从I节点中获取到的信息重组后放入参数buf指向的stat结构中(stat结构的说明在文件/usr/include/sys/stat.h中)。这两个调用成功时均返回0,否则返回-1。
stat与fstat调用无论在使用上还是在功能上都是非常类似的,在参数上有一点区别。下面我们来看一个例子。
/* statfile.c */
#include
#include
#include
#include
main(argc,argv)
int argc;
char *argv[];
{
int fd;
struct stat statbuf;
if (argc!=2){
printf("usage: statfile filename!\n");
exit(1);
}
if ((fd = fopen(argv[1], O_RDONLY)) == -1)
fprintf(stderr, "Cannot open %s!\n", argv[1]);
if (unlink(argv[1]) == -1)
fprintf(stderr,"Cannot unlink %s!\n", argv[1]);
if (stat(argv[1], &statbuf) == -1) /* by file name */
fprintf(stderr, "stat %s fails as it should !\n");
else
printf("stat %s succeed!\n", argv[1]);
if (fstat(fd, &statbuf) == -1) /* by file descriptor */
fprintf(stderr, "fstat %s fails!\n", argv[1]);
else
printf("fstat %s succeeds as it should!\n", argv[1]);
}
程序首先打开命令行中指定的文件,然后用unlink将该文件删除,接着分别用stat与fstat系统调用获取该文件的信息。假设当前命令下有一个名为xxx.c的文件,运行
%statfile xxx.c
后,将会输出如下结果:
stat xxx.c fails as it should!
fstat xxx.c succeeds as it should!
从中可知,当一个打开文件被删除后,用stat无法获取该文件的信息。而fstat就可获取该文件的信息。这是由于文件名在unlink之后已从目录中除去,无法找到该文件名,而文件描述字则因文件仍打开而保存下来。因此stat不能成功返回,但fstat仍可成功返回。
使用stat调用来判定一个文件为何种文件类型时相当有用。例如下列代码:
stat("hello", &statbuf);
if (statbuf.st_mod & S_IFMT) == S_IFDIR)
printf("This is a directory file!\n");
else if (statbuf.st_mod & S_IFMT) == S_IFREG)
printf("This is a regular file !\n");
else …
以上代码可判定当前目录下的hello文件是否为目录文件或其他类型的文件。
4.3 目录编程介绍
UNIX把目录也视为一种文件,称为目录文件,并同普通文件一样进行管理和保护。如open、close、read、lseek等文件操作对目录文件都是有效的。前面讲到的文件编程中的各个系统调用对于目录来说也同样有效。但是与普通文件相比目录文件又具有自身的一些特点:
目录文件的读/写/执行访问权限有特殊的含义:读权限允许用户读取目录项的内容;写权限允许用户创建或删除一个文件;执行权限则允许用户检索目录(此时通常称为目录搜索权限)。
目录的创建、删除与普通文件也不同,另外,任何用户都不能对目录文件以写方式打开进行文件写操作。
·目录的创建和删除-mkdir和rmdir系统调用
mkdir 和rmdir系统调用的格式为:
#include
#include
int mkdir (const char *path, mode_t mode);
#include
int rmdir(const char *path);
其中:参数path分别指出要创建和删除的目录文件的文件名。mkdir调用中的参数mode指出新创建目录文件的文件模式。新创建目录后,除"."和".."两项外,无别的目录项;删除目录时,要求目录中除"."和".."两项外,也无别的目录项。这两个系统调用Access成功时都返回0,否则返回-1。
·目录的读取-opendir/readdir/closedir库函数
目录文件可以像普通文件一样,先用系统调用open以读方式打开,再用read调用读取其中的内容。同时,由于目录文件是由具有目录结构的目录项组成的,用read读取其内容有些不方便。UNIX提供的库函数opendir/readdir/closedir等可以方便地实现目录读取。函数说明如下:
#include
#include
DIR *opendir(char *filename);
struct direct *readdir(DIR *dirp);
void closedir(DIR *dirp);
说明:参数filename指出要打开的目录路径名,库函数opendir返回一个指向结构DIR(在文件/usr/include/sys/dir.h中定义)的指针。库函数readdir和closedir均以这个指针作为参数,其中readdir返回一个指向结构direct的指针。有关目录的操作均可基于这个指针。下面是一个例子,查找当前目录下文件名为"name"的文件。
len = strlen(name);
dirp = opendir(".");
if (dirp == NULL) {
return NOT_FOUND;
}
while ((dp = readdir(dirp)) != NULL) {
if (dp->d_namlen == len && !strcmp(dp->d_name, name)) {
closedir(dirp);
return FOUND;
}
}
closedir(dirp);
return NOT_FOUND;
库函数closedir关闭打开的目录。值得注意的是,上面这一小段代码在编程中很实用,稍加修改即可实现UNIX下类似"ls"的简单命令。
第五章 基本进程编程
5.1 概述
UNIX系统为程序员提供了一个强有力的工具:在一个程序中执行另一个程序。执行一个程序最简单的途径就是使用库函数system。该函数使用一个参数:一个包含要被执行的命令的字符串。这一库函数的特点是用法简单,在程序中调用简单的UNIX命令时很有用。但是由于它的调用要由SHELL进程来实现,故效率并不高,在实际的编程中应用并不广泛。本章主要介绍在实际编程中经常使用的有关进程控制和管理方面的系统调用,它们包括:
fork - 创建一子进程
exec - 执行子进程
exit - 终止进程执行
wait - 等待子进程暂停或终止
setpgrp - 设置进程标识符
getpid、getppid - 获取进程标识符
setuid、setgid - 设置进程的用户标识符
getuid、geteuid、getgid、getegid - 获取进程的用户标识符
5.2 进程控制
1. fork系统调用
系统调用fork是UNIX操作系统创建新进程的唯一手段,习惯上将新创建的进程称为子进程,调用fork的进程称为父进程。
fork系统调用的格式为:
int fork()
fork系统调用没有参数,如执行成功,则创建一子进程,子进程继承了父进程的某些属性。当从该系统调用返回时,系统中已有两个用户级环境完全相同的进程存在。这两个进程从fork调用中得到的返回值不同,其中子进程得到的返回值为零,父进程得到的返回值是最新创建的子进程的进程标识符。
2. exec系统调用
fork系统调用只是将父进程的环境拷贝到新进程中,而没有启动执行一个新的目标程序。UNIX系统提供了exec系统调用,用它更换进程的执行映象,启动新的目标程序。例如:UNIX系统中的所有命令都是通过exec来执行的。
exec系统调用有六种不同的使用格式,但在核心中只对应一个调用入口。它们有不同的调用格式和调用参数。这六种调用格式分别为:
#include
int execl (const char *path, const char *arg0, ..., const char
*argn, (char *)0);
int execv (const char *path, char *const *argv);
int execle (const char *path, const char *arg0, ..., const char
*argn,
(char *0), const char *envp[]);
int execve (const char *path, char *const *argv, char *const *envp);
int execlp (const char *file, const char *arg0, ..., const char
*argn, (char *)0);
int execvp (const char *file, char *const *argv);
说明:参数path指出一个可执行目标文件的路径名;参数file指出可执行目标文件的文件名。arg0作为约定同path一样指出目标文件的路径名;参数arg1到argn分别是该目标文件执行时所带的命令行参数;参数argv是一个字符串指针数组,由它指出该目标程序使用的命令行参数表,按约定第一个字符指针指向与path
或file相同的字符串;最后一个指针指向一个空字符串,其余的指向该程序执行时所带的命令行参数;参数envp同argv一样也是一个字符指针数组,由它指出该目标程序执行时的进程环境,它也以一个空指针结束。
exec的六种格式在以下三点上有所不同:
(1) path是一个目标文件的完整路径名,而file是目标文件名,它是可以通过环境变量PATH来搜索的;
(2) 由path或file指定的目标文件的命令行参数是完整的参数列表或是通过一指针数组argv来给出的;
(3) 环境变量是系统自动传递或者通过envp来给出的。
下图说明了exec系统调用的六种不同格式对以上三点的支持。
系统调用 参数形式 环境传送 路径搜索
Execl 全部列表 自动 否
Execv 指针数组 自动 否
Execle 全部列表 不自动 否
Execve 指针数组 不自动 否
Execlp 全部列表 自动 是
Execvp 指针数组 自动 是
3. exit、wait系统调用
(1) exit系统调用格式如下:
#include
void exit(int status);
#include
void _exit(int status);
说明:exit的功能是终止进程的执行,并释放该进程所占用的某些系统资源。参数status是调用进程终止时传递给其父进程的值。如果调用进程执行exit系统调用时,其父进程正在等待子进程暂停或终止(使用wait系统调用),则父进程可立刻得到该值;如果此时父进程并不处在等待状态,那么一旦父进程使用wait调用,便可立刻得到子进程传过来的status值,注意:只有status的低八位才传递给其父进程。
系统调用_exit与exit之间的差异是_exit只做部分的清除,因此建议不要轻易地使用这种调用形式。
每个进程在消亡前都要调用该系统调用,没有显示地使用该系统调用,则生成目标文件的装载程序为该进程隐含地做这一工作。
(2) wait系统调用格式如下:
#include
#include
pid_t wait (int *statptr);
说明:wait系统调用将调用进程挂起,直到该进程收到一个被其捕获的信号或者它的任何一个子进程暂停或终止为止。如果在wait调用之前已有子进程暂停或终止,则该调用立刻返回。格式wait((int
*)0) 的功能是等待所有子进程终止。Wait返回时,其返回值为该子进程的进程号。参数statptr的值为该子进程的终止原因:
1、 如果子进程暂停,statptr目的高八位存放使该子进程暂停的信号值(在第七章中介绍信号),低八位为0177
2、 如果子进程由于调用exit终止,则该值的低八位为0,高八位为子进程终止时,exit系统调用中参数status值的低八位;
3、 如果子进程因信号终止,该值的高八位为0,低八位为引起终止的信号值。此外如低七位为1,则表示产生了一个core文件。
下面我们来看一个例子,该例是一个fork、exec、exit和wait联合使用的一个实例程序,我们称之为feew.c:
/* feew.c */
main(argc, argv)
int argc;
char *argv[];
{
int pid, stat;
if (argc != 1){
if ((pid = fork()) == 0){
printf("Child pid = %d\n", getpid());
execl(argv[1], argv[1], 0);
exit(5);
}
}
pid = wait(&stat);
printf("pid=%d, H_stat=%d, L_stat=%d\n", pid, stat>>8, stat&0xff);
}
当命令行参数的个数不为1时,程序使用fork系统调用产生一个子进程。子进程通过系统调用getpid获得自己的进程标识符,然后调用exec执行命令行中用户提交的命令,如果exec执行失败,则子进程调用exit(5)终止。父进程使用wait系统调用等待子进程暂停或终止,然后输出从wait中返回的信息。下面以三种方式执行该程序:
1〕 不带命令行参数
% ./feew
pid=-1, H_stat=0, L_stat=0
%
不产生子进程,从运行结果来看,当无子进程时,wait的返回值为-1。
2〕 带命令行参数,参数为合法的可执行命令
% ./feew /bin/date
Child pid = 1725
1998年 2月16日(星期一) 15时59分14秒 CST
pid=1725, H_stat=0, L_stat=0
%
产生子进程。子进程输出其进程标识符后,再调用exec执行从命令行中提交的命令(/bin/date),同时父进程等待子进程暂停或终止,然后输出从wait中得到的信息:子进程标识符或状态参数stat的高八位、低八位的内容。从中可以看到:子进程因调用一个隐含的exit(0)而终止,终止时传给父进程的值为0。
3〕 带命令行参数,但参数不合法
%./feew /etc/shudown
Child pid = 1760
/etc/shutdown: 只有超级用户(root)能运行 /etc/shutdown。
pid=1760, H_stat=2, L_stat=0
%
子进程创建成功。但由于以普通用户的身份执行/etc/shutdown,因此exec失败,尔后调用exit(5)而终止;父进程调用wait得到返回值:子进程号和状态参数stat的高八位、低八位的内容。从执行结果可以看出:子进程因调用exit(5)而终止,终止时传给父进程的值为5。
5.3 进程管理
进程管理包括的面很广,诸如进程的用户标识符管理、进程标识符管理等。进程的用户标识符有两个:实际用户标识符(real user
id)和有效用户标识符(effective user id),其对应的组标识符分别称为实际组标识符(real group
id)和有效组标识符(effective groud
id)。一般而言,进程的实际用户标识符为运行该进程的用户标识符,通常只用于系统记帐,其他功能由有效用户标识符来完成,如用有效用户标识符来完成对新创建文件赋予属性关系、检查文件的存取权限和利用kill系统调用向进程发送信号的权限。一般情况下,一进程的有效用户标识符和实际用户标识符是相等的,但系统允许改变进程的有效用户标识符。
1. 进程的用户标识符管理
UNIX系统提供了一组系统调用来管理进程的用户标识符,它们的使用形式是:
#include
#include
uid_t getuid (void);
uid_t geteuid (void);
gid_t getgid (void);
gid_t getegid (void);
int setuid(uid_t uid);
int setgid(gid_t gid);
说明:前四个系统调用没有参数,分别返回调用进程的实际用户标识符、有效用户标识符、实际用户组标识符和有效组标识符。这些系统调用的执行总能获得成功,不会发生任何错误。系统调用setuid和setgid用于设置进程的实际用户(组)标识符和有效用户(组)标识符,如调用进程的有效用户标识符是超级用户标识符,则将调用的进程实际用户(组)标识符和有效用户(组)标识符设置为uid或gid;如调用进程的有效用户标识符不是超级用户标识符,但其实际用户(组)标识符等于uid或gid时,则其有效用户(组)标识符被设置为uid或gid;否则setuid或setgid调用失败。系统调用setuid或setgid调用成功时返回0,失败时返回-1。
2. 进程标识符管理
UNIX系统使用进程标识符来管理当前系统中的进程。为对具有某类似特性的进程统一管理,系统又引入了进程组的概念,以组标识符来区别进程是否同组。进程的组标识符是从父进程继承下来的,所以,通常进程的组标识符就是和它相关联的注册进程的标识符。进程的标识符是由系统为之分配的,不能被修改;组标识符可通过setpgrp系统调用修改。
相关系统调用的格式如下:
#include
#include
pid_t getpid(void);
pid_t getpgrp(void);
pid_t getppid(void);
pid_t getpgid(pid_t pid);
说明:前三个系统调用分别返回调用进程的进程标识符、进程组标识符和其父进程标识符。它们总能成功地返回。第四个调用置进程组标识符,它将调用进程的进程组标识符改为调用进程的进程标识符,使其成为进程组首进程,并返回这一新的进程组标识符。
下面我们来看一个实例:
/* setuid.c */
main(argc, argv)
int argc;
char *argv[];
{
int ret, uid;
uid = atoi(argv[1]);
printf("Before uid=%d, euid=%d\n", getuid(), geteuid());
ret = setuid(uid);
printf("After uid=%d, euid=%d\n", getuid(), geteuid());
printf("ret = %d\n", ret);
}
下面分三种情况讨论该程序的执行:
1、 如果执行该程序的用户为超级用户,则只要命令行所给的用户标识符大于0,无论所给的用户标识符是否存在,执行总能成功。
#./setuid 3434
Before uid=0, euid=0
After uid=3434, euid=3434
ret = 0
#
结果分析:将进程的实际和有效用户标识符均改为3434。
2、 如果执行该程序的用户为一般用户,用id命令得到用户uid和gid后,再调用该程序,过程如下:
%id
uid=1111(yds) gid=20(user)
%./setuid 3434
Before uid=1111, euid=1111
After uid=1111, euid=1111
ret = -1
%./setuid 1111
Before uid=1111, euid=1111
After uid=1111, euid=1111
ret = 0
%
结果分析:当命令行参数为1111时,setuid执行成功,因为用户的uid就是1111。
值得注意的是:注册程序login
是个典型的setuid系统调用程序,login进程的有效用户是超级用户,该进程在建立用户的Shell进程前,调用setuid将实际和有效用户标识符调整为注册用户的用户实际和有效标识符。
第六章 设备输入/输出控制
6.1 概述
UNIX将设备看成文件,这是UNIX的一大特色。这里需要介绍一个设备号的概念。设备特别文件与两个设备号有关-主设备号和次设备号。主设备号告诉操作系统,当涉及文件名时,将使用哪种设备类型。对于每一种类型的设备都有一段驻留在操作系统中的程序代码,以控制相应类型的设备,这段代码被称为"设备驱动程序"。次设备号被传递给设备驱动程序,这个号码用来决定使用哪种物理设备。例如,决定在一块多重驱动控制卡上,哪个磁盘驱动器将被访问,以及该磁盘驱动器中哪一部分将被使用;或者,当一个磁盘驱动器所请求的操作已经完成后,应该被恢复原状。几个设备(如同类型的磁盘驱动器)可以用同一个主设备号,但它们将有不同的次设备号。看下面的例子:
%ls -l /dev/ttyq*
crw--w---- 2 yds user 15, 1 2月 17日 09时03分 ttyq1
crw--w---- 2 yds user 15, 14 2月 16日 17时00分 ttyq14
%
上例中15是主设备号,1和14是次设备号。
用户可以使用系统提供的统一而且独立于设备的界面-对文件进行操作的系统调用来操作设备,而没有必要涉及设备的具体细节。大部分对文件进行操作的系统调用对它们仍起作用,例如,用open打开设备,用read/write对设备进行读/写,设备操作完成后,用close关闭设备。但有的系统调用在对设备文件进行操作时,其功效有所不同。如create及open的创建方式都不能创建设备文件。
6.2 设备输入/输出控制-ioctl系统调用
ioctl是UNIX系统专门提供的用于设备控制的系统调用。该系统调用与设备类型(即主设备号)相关。不同的设备,系统提供了不同的控制命令。
ioctl的调用格式是:
ioctl(int fd, int cmd,arg…)
说明:参数fd是一设备文件的文件描述字,cmd是控制命令,它与设备相关,不同类型的设备有不同的控制命令。参数arg没有固定的数据结构,它随cmd的不同而不同。
第七章 高级编程
7.1 处理信号
信号是UNIX进程间最基本的通讯手段,主要作用是实现进程间异步事件的通讯。信号是传送到进程的"软中断",它通知进程在它们的环境中出现了非正常事件。进程接收到信号后要进行处理,处理方式为以下四种之一:
(1)
缺省方式(SIG_DFL):这是进程对信号的一般处理方式,在无特殊情况下,进程在接收到信号后将终止执行。有一些信号,在终止进程运行前需将终止进程的正文段、数据段、user结构和栈段内容写到当前目录的core文件中,以备调试工具分析与使用。
(2)
忽略方式(SIG_IGN):进程接收到一个已指明忽略的信号,则将该信号清除后,立即返回,不在任何工作。信号SIGKILL不能被忽略。
(3) 保持方式(SIG_HOLD):当进程处于该方式时,将接收的信号保存起来,等该进程的保持方式解除后,再进行处理。
(4)
捕获方式(设置信号处理函数):这是用户设置的信号处理方式,当进程接收到这种信号时,执行用户设置的信号处理函数,执行完后,恢复现场,然后继续往下执行。
1. 常用信号种类
UNIX信号的种类很多,下面介绍一些最常用的信号:
SIGHUP 挂断。这是当控制终端被挂起时送到进程的信号。
SIGINT 中断。由键盘产生的中断。
SIGQUIT 退出。由键盘产生的中断。
SIGKILL 终止。这个信号不能被捕获、阻塞或忽略。
SIGALRM 定时信号。
SIGTERM 软件终止信号。
SIGUSR1 用户定义的信号1。
SIGUSR2 用户定义的信号2。
这些信号值的声明在/usr/include/sys/signal.h文件中。
2. 发送信号-kill系统调用
用户传送信号到进程的系统调用是kill,调用格式为:
#include
#include
int kill (pid_t pid, int sig);
说明:该系统调用把一个信号值为sig的信号发送给进程标识符为pid的相关进程。成功时返回0,失败时返回-1。
该调用执行成功与否,依赖于调用进程的有效用户标识符和参数pid的值,pid值的含义如下:
大于0:将信号发送给进程号等于pid的进程。
等于0:将信号发送给调用进程的同组进程(0和1进程除外)。
等于-1:将信号发送给实际用户标识符等于调用进程的有效用户标识符的所有进程(0和1进程除外),如调用进程的有效用户是超级用户,则将信号发送给除0和1进程外的所有进程。
非-1的负数:将信号发送给进程组标识符为pid的绝对值的所有进程。
在实际编程中,kill系统调用非常有用,具体说来:
·常用方式
kill(pid,SIGUSR1) 向进程号为pid的进程发送信号SIGUSR1
·用来判断进程是否存在:
if (kill(pid,0) == 0)
进程号为pid的进程存在;
else
进程号为pid的进程不存在!
·用来杀掉子进程
kill(pid,1) 杀掉进程号为pid的进程
2. 处理信号-signal系统调用
用户处理信号的系统调用是signal,调用格式为:
#include
void (*signal (int sig, void (*func)()))();
说明:参数sig是一个信号值,func定义了该信号的处理方式。该系统定义的功能是按func的定义设置调用进程对信号sig的处理方式。执行成功时,返回调用进程先前对信号sig处理方式的值,失败则返回-1。参数取值为SIG_DFL或SIG_IGN或用户信号处理函数的地址时,分别表示缺省方式、忽略方式和捕获方式。
3.pause系统调用
pause系统调用的格式为:
pause()
说明:该调用没有参数,其功能为使调用进程睡眠直到其接收到一信号为止。该系统调用的结果依赖于调用进程对接收到的信号的处理方式。
缺省方式:终止调用进程,pause无返回值;
忽略方式:进程不受该信号的影响,继续睡眠;
捕获方式:调用进程从信号处理函数返回后,继续往下执行。
4. 使用信号定时-alarm系统调用
系统调用alarm可以实现定时器的功能,调用格式为:
#include
unsigned alarm(unsigned sec);
说明:参数sec指定定时的时间间隔,以秒为单位。用户进程可以先通过signal调用指定SIGALRM信号对应的捕获函数,然后调用alarm来设定闹钟,在定时这段时间内做自己的工作。定时时间一到,进程就接收到一个SIGALRM信号,并执行该信号对应的捕获函数。系统调用alarm在多进程编程中非常有用。
7.2 管道通讯
用信号来处理异常事件或错误是非常合适的,但它用来处理进程之间的大量信息传送,就非常不适宜。为此,UNIX又提供了一种称为管道的机构,主要处理进程间的大量信息传送。所谓管道是指进程间连接起来的一条通讯通道。它也UNIX文件概念的一种推广,管道通讯的介质是文件,称为管道文件。用户可以用文件操作的有关系统调用来操作管道文件,从而简化管道应用程序的设计。管道的形象描述如下图:
write 写端 读端 read
管道是UNIX最强大而最有特色的性能之一,特别是在命令行这一级,它允许任意的命令被顺序连接起来。例如:
%who | wc -l
该命令通过管道把命令who的输出送给字计数程序wc,选项-l告诉wc只计算行数。通过wc最终输出的系统已注册的用户个数。
1. 管道程序设计
在程序中可以用系统调用pipe建立一个管道。如果建立成功,就返回两个文件描述符,一个用于写入管道,一个用于从管道中读出。Pipe调用的格式如下:
int filedes[2], retval;
retval = pipe(filedes);
其中,fildes是一个含有两个整数的数组,用来存放标识管道的两个文件描述符。如果调用成功,filedes[0]将被打开用于从管道读,fildes[1]将被打开用于向管道写。
管道一旦建立,就能直接用read和write操作它。当管道与系统调用fork联用时,才能体现出管道的真正价值。这时,可以利用父进程已打开的文件,对于其子进程仍保持打开这一事实。下面的程序先建立一个管道,然后调用fork创建子进程,父进程通过管道向子进程发送信息。
/* pipe.c */
#include
#define MSGSIZE 16
char *msg1 = "hello, world#1";
char *msg2 = "hello, world#2";
char *msg3="hello, world#3";
main(argc,argv)
int argc;
char **argv;
{
char inbuf[MSGSIZE];
int p[2], pid,j;
/* 打开管道 */
if (pipe(p) < 0){
perror("pipe call");
exit(1);
}
if ((pid = fork()) <0 ){
perror("fork call");
exit(2);
}
/* 在父进程中向管道写入 */
if (pid >0 ){
write(p[1], msg1, MSGSIZE);
write(p[1], msg2, MSGSIZE);
write(p[1], msg3, MSGSIZE);
wait((int *)0);
}
/* 在子进程中从管道读入 */
if (pid == 0){
for (j=0; j<3; j++){
read(p[0], inbuf, MSGSIZE);
printf("Child Read:%s\n", inbuf);
}
}
exit(0);
}
程序的输入结果如下:
Child Read:hello, world#1
Child Read:hello, world#2
Child Read:hello, world#3
管道是在先进先出的基础上处理数据的。所以,首先放入管道的数据,在其另一端首先被读出。这个顺序不能被改变,因为系统调用lseek不能用于管道。
2. 命名管道-FIFO
我们已经看到,管道是一种功能很强的进程通讯机构。但是,它也存在一些严重的缺点。
首先,管道只能用于连接具有共同祖先的进程,如父子进程之间的连接。当要开发一个永远保持存在的,提供为全系统范围服务的程序时,这一缺点就更加突出,例如网络控制服务程序和打印机的假脱机程序等。我们要求调用进程应该能够用管道与任何服务进程进行通讯,然后再脱开。遗憾的是,普通管道不能实现上述功能。
其次,管道不能是常设的,在需要时可以建立它们,但是当访问它们的进程终止时,管道也随之被撤销。所以,它们不可能永久存在。
事实上,UNIX系统中的FIFO机制(又称命名管道),弥补了上述管道的不足之处。FIFO与管道一样,也是作为进程之间先进先出的通讯通道,但是FIFO是一种永久性的机构,并且具有一个UNIX文件名。FIFO也具有文件主、长度和访问权限。它能象其他UNIX文件那样被打开、关闭和删除。但在读和写时,其性能与管道相同。
在讨论FIFO程序设计之前,我们先来看一下FIFO在命令级的使用。UNIX命令mknod可以用来创建一个FIFO文件channel:
%/etc/mknod channel p
%ls -l channel
prw-r--r-- 1 yds user 0 2月 17日 14时19分 channel
命令ls的输出结果中的首字母p指出channel是一个FIFO类型的文件。从中我们还可以看到其访问权限为文件主可读写,组内及其他用户只读。其用户主是yds,所属组为user,长度为0,此外还有文件建立的时间。
FIFO程序设计大部分与管道相同,最主要的区别是在建立方面。FIFO是用mknod调用建立的,而不是用pipe建立的。另外,必须把八进制数010000(定义在文件/usr/include/sys/stat.h的常量S_IFIFO中)加入文件模式中,以指明这是一个FIFO。下面是一个建立FIFO的例子:
if (mknod("fifo", 010600,0) < 0)
perror("mknod(fifo) call");
这个例子建立一个名为fifo的FIFO,其权限为0600,所以此FIFO可以被其文件主读写。一旦建立一个FIFO,必须用系统调用open打开它,例如:
#include
.
.
fd = open("fifo", O_WRONLY);
实现打开一个FIFO文件用于写,下面的例子用于以非阻塞方式打开FIFO文件用于读:
if ((fd = open("fifo", O_RDONLY | O_NDELAY)) < 0)
perror("open on file");
下面介绍两个程序,说明FIFO的基本应用。值得注意的是,这两个程序构成了FIFO编程的基本框架,稍微修改即可用于其他场合的FIFO应用。
首先是sendfifo.c的程序清单,用于向FIFO文件写入字串:
/* sendfifo.c */
#include
#include
#include
#define MSGSIZ 63
extern int errno;
main(argc,argv)
int argc;
char **argv;
{
int fd;
char buf[MSGSIZ+1];
int i,nwrite;
if (argc < 2){
fprintf(stderr,"Usage : sendfifo msg ...!\n");
exit(1);
}
if ((fd = open("fifo", O_WRONLY | O_NDELAY)) < 0)
printf("fifo open failed!");
for ( i =1 ; i< argc; i++){
if (strlen(argv[i]) > MSGSIZ) {
fprintf(stderr, " message too long %s!\n", argv[i]);
continue;
}
strcpy(buf, argv[i]);
if ((nwrite = write(fd,buf,MSGSIZ+1)) <= 0){
if (nwrite == 0) /* full FIFO */
errno = EAGAIN;
printf("message write failed!");
}
}
}
下面是recvfifo.c的程序清单,实现从FIFO的读入:
#include
#include
#define MSGSIZ 63
main(argc,argv)
int argc;
char **argv;
{
int fd;
char buf[MSGSIZ+1];
mknod("fifo",010600,0);
if ((fd = open("fifo", O_RDWR)) < 0)
printf("fifo open failed!");
for (;;){
if ( read(fd,buf,MSGSIZ+1) < 0)
printf("message read failed!");
printf("FIFO message received: %s\n" , buf);
}
}
运行结果如下:
%recvfifo &
[1] 1706
%sendfifo hello world
FIFO message received: hello
FIFO message received: world
%
首先,运行recvfifo程序创建FIFO文件"fifo",并打开文件"fifo"用于读;然后,运行sendfifo程序发送字符串"hello
world",写入文件"fifo"中。
7.3 IPC通讯机制
1. IPC概述
IPC是UNIX
系统V提供的一套新的进程间通讯进制,它大大增强了进程间的通讯功能。IPC机构包括三种:消息、信号量和共享内存。三种IPC机构的程序设计接口比较相似,这说明它们的内核实现是相似的。IPC最重要的通用特性就是键,键是UNIX系统中标识IPC目标的一个数,其方式类似于一个文件名标识一个文件。也就是说,键可以使多个进程容易共享IPC资源。键所标识的目标可以是一个消息队列、一组信号量或一个共享内存段。键的实际数据类型由实现有关的类型key_t决定,它在头文件/usr/include/sys/types.h中被定义。
当建立一个IPC目标时,系统也建立了一个IPC机构的状态结构,其中包含该目标有关的管理信息。对于消息队列、信号量和共享内存均有一种状态结构类型,每种类型必须含有仅与特定IPC机构有关的信息。但是,这三种状态结构类型都有有关权限结构,这种权限结构的类型用ipc_perm来标识,它包含以下内容:
u_short cuid /* IPC目标创建者的用户ID */
u_short cgid /* 创建者的用户组ID */
u_short uid /* 有效用户ID */
u_short gid /* 有效用户组ID */
u_short umode /*权限许可 */
该结构决定一个用户是否能对IPC目标进行读/写。权限的组成方法与文件的权限完全一样。所以,如果umode之值为0644,则表示属主能读写相应的目标,而其他用户只能读。注意,有效用户标识符和有效组标识符(记录在uid和gid内)与umode一起确定访问的许可性。
最后,IPC的每种形式都提供了各种操作功能,以便IPC进制可被使用。信息队列操作允许消息发送和接收。信号量操作允许信号量增加、减少以及检测到某个值。共享内存操作功能允许进程加上和减去共享内存的部分到它们的地址空间。
2. 消息队列
从本质上看,一个消息是一串字符或字节(不一定以NULL字符结尾)。进程之间通过消息队列传送消息。通过msgget建立或访问消息队列。一个消息队列一旦被建立,只要符合访问权限,进程就可以通过msgsnd把消息放入队列,另一个进程就能用msgrcv读出该信息。
·msgget系统调用
msgget调用格式如下:
#include
#include
#include
int msgget(key_t key, int msgflg);
说明:参数key是标识消息队列的键。如果该调用成功,就建立一个消息队列,或者使一个已经存在的消息队列能够被访问。调用返回一个该消息队列的标识符。参数msgflg确定msgget完成的动作。可以取两个常数:
(1)
IPC_CREAT:创建消息队列,且在消息队列已经存在的情况下,不会被重写。如果没有设置该标志,那么当队列已存在时,msgget就返回该消息队列的标识符。
(2)
IPC_EXCL:如果该标志与IPC_CREAT都被设置,本次msgget调用则只希望建立一个消息队列。所以,当给出的键值已对应一个存在的消息队列时,调用失败,并返回-1。
建立一个消息队列时,msgflg的低9位用来写出消息队列的权限,这与文件模式一样。如:
msg_id = msgget((key_t)0100, 0644 |IPC_CREAT|IPC_EXCL);
这个调用为键值(key_t)0100建立一个消息队列。如果调用成功,队列的权限为0644,其解释与文件权限一样。
·msgget和msgrcv系统调用
msgsnd和msgrcv调用格式如下:
#include
#include
#include
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int
msgflg);
说明:参数msqid指明消息发送或接收的队列,它的值是通过msgget调用得到的。消息的结构类型如下:
struct {
long mtype; /* 消息类型 */
char mtext[]; /* 消息正文 */
}
程序员可以根据这个结构中的mtype域来对消息进行分类。该域的每种可能的值代表一种不同的类别。mtext域用来存放消息正文,正文大小可用用户设定。
系统调用msgsnd的参数msgsz指定发送消息的实际长度,其范围可以从0到系统规定的消息最大长度。系统调用msgrcv中的参数msgsz指定给出了结构内能存放消息的最大长度。如果调用成功,msgrcv返回接收到的消息的实际长度。
两个系统调用中的参数msgflg中有个IPC_NOWAIT。如果没有设定它,那么调用进程就会进入睡眠状态。否则调用就会立即返回。
·msgctl系统调用
msgctl调用格式如下:
#include
#include
#include
int msgctl(int msqid, int cmd, .../* struct msqid_ds *buf */);
说明:msgctl用来获取和修改一个已经存在的消息队列的属性。参数msgqid是消息队列的ID,命令常量cmd的取值有三种:
(1) IPC_STAT:放置关于结构中消息队列当前消息的一个备份。
(2) IPC_SET:为消息队列设置控制变量值。
(3) IPC_RMID:从系统中删除消息队列,但是只有超级用户或队列属主才能实现。
3. 信号量(略)
4. 共享内存
共享内存操作允许两个或两个以上进程共享一个物理存贮器段,它是所有IPC中效力最高的一种。一个共享内存段被唯一的标识符所描述。
·shmget系统调用
shmget调用格式如下:
#include
#include
#include
int shmget(key_t key, size_t size, int shmflg);
说明:参数key是标识共享内存的键。参数size是创建或访问共享内存的大小。如果调用成功,就创建一块共享内存,或者使一块已经存在的共享内存能够被访问。调用返回一个该共享内存的标识符。参数shmflg同调用msgget,semget中的参数msgflg,
semflg一样。
·shmat和shmdt系统调用
shmat和shmdt调用格式如下:
#include
#include
#include
void *shmat(int shmid, void *shmaddr, int shmflg);
int shmdt (void *shmaddr);
说明:shmat调用把参数shmid标识的内存段连到调用进程的一个有效地址上。调用成功,shmat返回该地址memptr。参数shmaddr给出程序员在调用所选地址的控制。参数shmflg由标志SHM_RDONLY和SHM_RND构成。前者请求被连之段为只读,后者用于shmat处理shmaddr非0的情况。
Shmdt的功能与shmat刚好相反,它实现把一个共享内存段从进程的逻辑地址空间中分离出来。这意味着进程将不再使用它。
·shmctl系统调用
shmclt调用格式如下:
#include
#include
#include
int shmctl (int shmid, int cmd, .../* struct shmid_ds *buf */);
说明:这个调用实现对共享内存的操作控制,其使用与msgctl完全一样,其参数cmd可以取IPC_STAT、IPC_SET和IPC_RMID。
第八章 网络编程
8.1 概述
本章介绍UNIX网络编程-即网间进程通讯。UNIX网间进程通讯是通过通讯应用程序接口(API)来实现的。目前,在UNIX环境下最流行的API是伯克利套接字(Socket)和UNIX
System V的传送层接口(TLI)。我们主要介绍套接字API。
Socket通过域 (domain)来划分所支持的协议, 目前支持的域有:
UNIX域支持在UNIX系统中的进程通讯、Internet域支持TCP/IP协议等。
Socket的实现者试图以UNIX文件的操作语义来模拟进程通讯的操作,其操作方式与文件操作有许多对应。例如,socket(
)调用可近似的看成是open( )调用,调用返回的文件描述字作为其他调用的第一参数;socket 中也使用了read 和write
调用,其语法和语义与文件操作中的read 和 write 调用几乎完全一致。Socket中的调用 bind、connect和accept
显示了建立网络连接的方法。 如图8-1所示。Socket进程通讯仍使用Client/Server 模型,建立连接时,Client 和
Server 所做的工作是不对称的。
8.2 套接字编程接口说明
下面结合实例来说明套接字编程接口。
·socket系统调用
实现套接字的分配,调用格式如下:
#include
#include
int socket(int domain, int type, int protocol);
其中:参数domain是一个常量,它规定区域,常用的是AF_INET;参数type是一个常量,规定套接字的类型,可以是SOCK_STREAM,SOCK_DGRAM或SOCK_RAW;protocol是一个常量,规定所用的协议。此参数仅在type为SOCK_RAW时有意义,其他情况下忽略。此参数为0时选择默认协议。
·bind系统调用
当应用程序获得套接字后,可以使用bind()调用为套接字联系一个独一无二的名字,如下面一段代码:
struct sockaddr_in serverAddress ;
memset( (char *)&serverAddress , 0 , sizeof(struct sockaddr_in) ) ;
serverAddress.sin_family =AF_INET ;
serverAddress.sin_addr.s_addr =inet_addr("202.96.6.15") ;
serverAddress.sin_port = htons( 7000);
if( bind( sockfd , &serverAddress ,sizeof( struct sockaddr_in ) )
==-1 )
{
perror( "bind error" ) ;
exit( 2 ) ;
}
这段代码说明SERVER程序运行在IP地址202.96.6.15,端口号为7000上。Bind调用之后相当于将自己的服务地址公布出去。
bind的调用格式如下:
#include
#include
int bind (int s, const struct sockaddr *name, int namelen);
其中:参数s是socket调用返回的文件描述字,参数name是指向结构sockaddr的指针,参数namelen指定结构的大小。
·listen系统调用
在bind调用之后,SERVER程序使用listen调用来准备接收来自CLIENT的连接。listen的调用格式如下:
#include
#include
int listen (int s, int backlog);
其中:参数s是socket调用返回的文件描述字,参数backlog指定最大连接数。
·accept系统调用
在listen调用之后,SERVER程序使用accept调用实际接收来自CLIENT的连接请求。accept的调用格式如下:
#include
#include
int accept (int s, struct sockaddr *addr, int *addrlen);
其中:参数s是socket调用返回的文件描述字,参数addr指向结构sockaddr,负责读入CLIENT端的相应信息。参数addrlen指出addr对应结构的长度。
·connect系统调用
在CLIENT方,调用socket之后,就可使用connect调用向SERVER初始化一个连接请求。如下面的代码:
struct sockaddr_in serverAddress ;
memset( (char *)&serverAddress , 0 , sizeof(struct sockaddr_in) ) ;
serverAddress.sin_family =AF_INET ;
serverAddress.sin_addr.s_addr =inet_addr("202.96.6.15") ;
serverAddress.sin_port = htons( 7000);
if( connect( sockfd , &serverAddress ,sizeof( struct sockaddr_in ) )
==-1 )
{
perror( "bind error" ) ;
exit( 2 ) ;
}
这段代码完成了向运行在IP地址202.96.6.15,端口号为7000上的SERVER程序建立连接。
connect的调用格式如下:
#include
#include
int connect (int s, const struct sockaddr *name, int namelen);
其中:参数s是socket调用返回的文件描述字,参数name是指向结构sockaddr的指针,参数namelen指定结构的大小。
·read/write/close系统调用
与普通文件操作类似。
:em23: