整理了一篇UNIX编程的文章,拿出来和大家共享 第六章 设备输入/输出控制 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 <sys/types.h> #include <signal.h> 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 <signal.h> 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 <unistd.h> 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 <stdio.h> #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 <fcntl.h> . . 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 <fcntl.h> #include <stdio.h> #include <errno.h> #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 <fcntl.h> #include <stdio.h> #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 <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> 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 <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> 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 <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> 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 <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg); 说明:参数key是标识共享内存的键。参数size是创建或访问共享内存的大小。如果调用成功,就创建一块共享内存,或者使一块已经存在的共享内存能够被访问。调用返回一个该共享内存的标识符。参数shmflg同调用msgget,semget中的参数msgflg, semflg一样。 ·shmat和shmdt系统调用 shmat和shmdt调用格式如下: #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> 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 <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> 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 <sys/types.h> #include <sys/socket.h> 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 <sys/types.h> #include <sys/socket.h> int bind (int s, const struct sockaddr *name, int namelen); 其中:参数s是socket调用返回的文件描述字,参数name是指向结构sockaddr的指针,参数namelen指定结构的大小。 ·listen系统调用 在bind调用之后,SERVER程序使用listen调用来准备接收来自CLIENT的连接。listen的调用格式如下: #include <sys/types.h> #include <sys/socket.h> int listen (int s, int backlog); 其中:参数s是socket调用返回的文件描述字,参数backlog指定最大连接数。 ·accept系统调用 在listen调用之后,SERVER程序使用accept调用实际接收来自CLIENT的连接请求。accept的调用格式如下: #include <sys/types.h> #include <sys/socket.h> 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 <sys/types.h> #include <sys/socket.h> int connect (int s, const struct sockaddr *name, int namelen); 其中:参数s是socket调用返回的文件描述字,参数name是指向结构sockaddr的指针,参数namelen指定结构的大小。 ·read/write/close系统调用 与普通文件操作类似。
|