860 likes | 1.02k Views
操作系统. 第十四章 UNIX 系统程序设计. 陆松年 snlu@sjtu.edu.cn. 系统调用返回值 ( page 355). 大多数的系统调用都返回一个值。如打开一个文件的系统调用 fd=open ( name , mode )返回一个文件描述符 fd 。 当指定的文件不存在时返回 -1 。返回值 -1 指示一个系统调用可能失败了,这时系统的全局变量 errno 值为相应的出错代码。 系统还定义了另外两个外部变量,即对应于出错代码的消息数组 sys_errlist 和比该数组最大下标大 1 的整型变量 sys_neer 。
E N D
操作系统 第十四章 UNIX系统程序设计 陆松年 snlu@sjtu.edu.cn
系统调用返回值 (page 355) • 大多数的系统调用都返回一个值。如打开一个文件的系统调用fd=open(name,mode)返回一个文件描述符fd。 • 当指定的文件不存在时返回 -1。返回值 -1指示一个系统调用可能失败了,这时系统的全局变量errno值为相应的出错代码。 • 系统还定义了另外两个外部变量,即对应于出错代码的消息数组sys_errlist和比该数组最大下标大1的整型变量sys_neer。 • 为了在系统调用失败后获得出错代码和出错信息,我们可以编写一个如下的C函数:
#include <stdio.h> void syserr(syscall) char *syscall; { extern int errno,sys_nerr; extern const char *const sys_errlist[]; fprintf(stderr,"ERROR: %s %d", syscall,errno); if(errno>0 && errno<sys_nerr) fprintf(stderr,"--%s\n", sys_errlist[errno]); else fprintf(stderr,"\n"); exit(1); }
14.3 高级的进程间通信 (p316) 利用消息通信,进程可以将具有一定格式的消息发送给任意进程。UNIX系统V为消息通信提供了四个系统调用,还要涉及到下面几个头文件。 #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> 14.3.1 消息通信
1.生成一个消息队列 int msgget(key,flags) /* 获取消息队列标识数 */ key_t key; /* 消息队列关键字,长整型 */ int flags; /* 操作标志 */ 参数key是通信双方约定的消息队列关键字,它是一个非负长整数。UNIX IPC通信机构将根据它生成一个消息队列,并返回一个队列标识数ID。 队列ID与文件描述字相似,但进程只要知道该值就可以使用它,不必像文件ID那样只有通过继承才能对同一个文件操作。当指定关键字的消息队列存在时,msgget就简单地返回该队列的ID。
参数flags类似打开连创建文件的格式中第二个参数o_flags和mode的组合。参数flags类似打开连创建文件的格式中第二个参数o_flags和mode的组合。 • flags的低9位与文件的存取模式相似,分别说明消息队列的属主用户、同组用户和其他用户对该队列的建立和访问控制。 • flags中的IPC_CREAT位(01000)如设置,可用于建立一个新的消息队列,或返回一个已存在的消息队列描述字。 • 如没设置,则该队列必须已存在,在这种情况下,msgget只能用于将已存在的队列的关键字映射为队列ID。 • 在IPC_CREAT标志和IPC_EXCL标志同时设置的情况下,如指定关键字的消息队列已存在,则出错返回(-1)。 • 如参数key等于IPC_PRIVATE(0),IPC机构则创建一个新的消息队列(与flags中的IPC_CREAT标志无关),并为该队列分配一个关键字,这可避免同一个已存在的队列发生冲突。
2.向消息队列发送一个消息 int msgsnd(qid,buf,nbytes,flags) int qid,nbytes,flags; struct msgbuf *buf; • 参数qid是消息队列ID,nbytes是消息正文的长度,flags是发送标志。 • 如flags为零,当消息队列满时进程阻塞自己。如flags中IPC_NOWAIT(04000)置位,消息队列满时msgsnd返回-1,不阻塞进程。 • 参数buf指定一个由用户定义的消息结构,其基本格式为: struct msgtype { long mtype; char data[NBYTES]; }; • 注意,发送进程对消息队列必须有写权限。
3.从消息队列接收一个消息 int msgrcv(qid,buf,nbytes,mtype,flags) int qid,nbytes,flags; long mtype; struct msgbuf *buf; • msgrcv中的参数与msgsnd类似,flags中如MSG_NOERROR置位,则允许所接收的长度nbytes小于消息正文长度,而不作为出错(返回-1)处理。 • buf所指的空间大小为不包括mtype的最大消息正文长度,实际接收的消息长度由msgrcv返回值指出。 • mtype为0时接收消息队列中最早的消息,而不管消息的类型是什么,否则只接收指定类型的消息。
4. 消息队列控制 int msgctl(qid,cmd,sbuf) int qid, cmd; struct msqid_ds *sbuf; • msgctl询问队列ID为qid的消息队列的各种特性或对其进行相应的控制。 • msqid_ds是消息队列定义的控制结构,其中包含存取权限结构、队列容量、进程标识和时间等信息。当cmd取值为: IPC_RMID(值为0):删除指定的消息队列,释放消息队列标识符。 IPC_SET(值为1):将sbuf中的控制信息写到消息队列控制结构中。 IPC_STAT(值为2):将消息队列控制结构中的信息写到sbuf中。 • 注意,后两种操作仅由能消息队列的创建者、属主或特权用户执行。
下面是一个请求进程和服务进程利用消息机构实现进程通信的例子。通信双方通过关键字为MSGKEY的消息队列进行通信,两个程序使用相同的一组头文件,假设头文件的说明放在文件msgcom.h中:下面是一个请求进程和服务进程利用消息机构实现进程通信的例子。通信双方通过关键字为MSGKEY的消息队列进行通信,两个程序使用相同的一组头文件,假设头文件的说明放在文件msgcom.h中: /* msgcom.h */ #include <errno.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #define MSGKEY 5678 struct msgtype{ long mtype; int text; };
/* 请求进程 */ #include "msgcom.h" main() { struct msgtype buf; int qid,pid; qid=msgget(MSGKEY,IPC_CREAT|0666); buf.mtype=1; buf.text = pid = getpid(); msgsnd(qid,&buf,sizeof(buf.text),0); msgrcv(qid,&buf,512,pid,MSG_NOERROR); printf("Request received a massags from server, type is:%d\n",buf.mtype); }
/* 服务器进程 */ #include "msgcom.h" main() { struct msgtype buf; int qid; if((qid=msgget(MSGKEY,IPC_CREAT|0666))==-1) return(-1); while(1){ msgrcv(qid,&buf,512,1,MSG_NOERROR); printf("Server receive a request from process %d\n",buf.text); buf.mtype=buf.text; msgsnd(qid,&buf,sizeof(int),0); } }
在上面的例子中,请求进程向服务器进程发送类型为1、正文为本进程标识数的消息。在上面的例子中,请求进程向服务器进程发送类型为1、正文为本进程标识数的消息。 服务器进程在收到了消息后,回发类型为请求进程标识数的消息。 我们同时也可看出,消息的正文格式可由用户自行定义。
14.3.2 共享内存 • FIFO和消息机构所提供的进程间通信的方法是要通过系统调用将数据从一个空间搬至另一个空间。 • 在进程间传递数据的最快方法是让一些相关进程直接共享某些内存区域,从而就根本不必移动数据本身。 • 系统V支持任意数目进程对内存的共享。每一个共享内存区域称为共享段,一个进程可以访问多个共享段。共享内存涉及的头文件和系统调用是: #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h>
1. 创建一个共享内存段 int shmget(key,nbytes,flags) key_t key; /* 共享内存段关键字 */ int nbytes,flags; /* 长度、标志 */ • 这三个参数的含义与消息通信中的系统调用msgget类似。 • key取值IPC_PRIVATE时,新创建的共享内存段的关键字由系统分配。 • shmget创建共享内存段成功时,初始化相应的控制信息,返回该共享段的描述字ID。
2. 将共享内存段映射到进程的虚地址空间 char *shmat(segid,addr,flags) int segid,flags; char *addr; • shmat将标识字为segid的共享内存段映射到由addr参数指定的进程虚地址空间。该地址空间可通过brk或sbrk系统调用动态分配而得。 • 如果你不关心映射内存的地址,可置addr为0,让系统选择一个可用地址。 • shmat调用成功后返回共享内存段在进程虚地址空间的首地址。 • 参数flags中如SHM_RDONLY置位,则以只读方式映射,否则以读写方式映射。 • 一个进程可以对同一个共享正文段调用多次shmat,以将它映射到本进程的不同虚地址空间内。
3. 解除共享内存段的映射 int shmdt(addr) int char *addr; /* 共享内存段虚地址 */ • 参数addr是相应的shmat调用的返回值。shmdt调用成功时,内存段的访问计数减1,返回值为0。 • 即使访问计数为0,如该共享内存段含有“不可释放”(IPC_NORMID)标志,那么实际的共享段仍存在,以后进程还可再映射它。 • 在程序中也可不用shmdt来显式解除共享段的映射,当进程终止时,系统将自动解除存在的映射关系。
4. 共享内存段控制 int shmct(segid,cmd,sbuf) int segid,cmd; /* 标识字,控制字 */ struct shmid_ds *sbuf; /* 指向共享内存段控制结构*/ SHM_LOCK:将共享段锁定在内存,禁止换出(超级用户才具有本权限)。 SHM_UNLOCK:与lock相反(超级用户才具有本权限)。 IPC_RMID,IPC_STAT,IPC_SET:类似于msgctl中定义, 其中IPC_RMID标志使得所对应的存储段标志成“可释放”。 • 进程将共享内存段映射到用户地址空间后,随后对共享内存的访问是与访问局部变量一样快。但对共享段的数据存取的互斥或同步操作要靠进程间协议自行解决。
14.3.3 信号灯 • 进程间的互斥和同步可利用Wait、Signal操作实现,但UNIX系统并没有直接向用户提供这两个操作,而是提供了一组有关信号灯的系统调用。 • 与一般的信号灯相比,系统V中的信号灯机构功能更强,管理和使用也较复杂,用户可以一次对一组信号灯进行相同或不同的操作,每个操作可以对信号灯的值加减任意正整数。 • 下面给出有关信号灯的头文件和系统调用。 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h>
1.创建一个信号灯组 int semget(key,nsems,flags) key_t key; /* 信号灯组关键字 */ int nsems,flags; /* 信号灯个数、 操作标志 */ • 当key为IPC_PRIVATE时,信号灯组关键字由系统选择。 • flags决定信号灯组的创建方式和权限,其取值和含义与msgget中的flags类似。 • semget调用成功时初始化相应的控制块信息,返回信号灯组标识数。
2. 对信号灯组的操作 int semop(sid,ops,nops) int sid; /* 信号灯组标识符 */ struct sembuf **ops; /* 对信号灯组 进行操作的数据结构 */ unsigned nops; /* 操作个数 */ • semop根据sembuf型的结构数组对标识数为sid的信号灯组中的信号灯进行块操作。 • 在sembuf结构中定义了对编号为sem_num的信号灯要进行的操作。
struct sembuf { short sem_num; /* 信号灯编号, 从0开始 */ short sem_op; /* 信号灯操作数 */ short sem_flg; /* 操作标志 */ }; • sem_op取正或负值时,一般意义为使对应的信号灯增加或减少该值, • 取值为0时仅对信号灯值进行测试。 • IPC_NOWAIT是否置位决定在对信号灯进行操作后,如其值小于0,进程是否要睡眠等待。
3. 信号灯控制 int semctl(sid,snum,cmd,arg); int sid,snum,cmd; /* 信号灯组ID, 信号灯编号,控制信令 */ union semun arg; 联合semun的格式为: union semun { • int val; struct semid_ds *buf; /*指向信号灯集 控制块的指针 */ ushort *array; };
在semctl调用中,系统根据cmd来解释arg,cmd的主要取值及相关的arg含义为:在semctl调用中,系统根据cmd来解释arg,cmd的主要取值及相关的arg含义为: GETVAL:取信号灯(sid,snum)的值,存入arg.val。 SETVAL:将信号灯(sid,snum)的值置为arg.val, 用于对信号灯初始化。 GETALL:将信号灯组(sid)中所有信号灯的值取到 arg.array[]中。 SETALL:将信号灯组(sod)中所有信号灯的值设置 为arg.array[]中的值。 IPC_STAT:将信号灯组(sid)的状态信息取到buf结构 中。 IPC_SET:将信号灯组(sid)的状态信息设置为buf 结构中的信息。 IPC_RMID:删除信号灯组的标识数。
用信号灯实现Wait、Signal操作的例子 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h>
int creatsem(key) key_t key; { int sid; union semun { /* 如sem.h中已定义,则省略 */ int val; struct semid_ds *buf; ushort *array; } arg; if((sid=semget(key,1,0666|IPC_CREAT))==-1) syserr("semget"); arg.val=1; if(semctl(sid,0,SETVAL,arg)==-1) syserr("semctl"); return(sid); }
void Wait(sid) int sid; { static void semcall(); semcall(sid,-1); } void Sinnal(sid) int sid; { static void semcall(); semcall(sid,1); }
static void semcall(sid,op) int sid,op; { struct sembuf sb; sb.sem_num = 0; sb.sem_op = op; sb.sem_flg = 0; if(semop(sid,&sb,1) == -1) syserr("semop"); }; • 在上面的几个函数中,creatsem用于建立只有一个信号灯的信号组,再将该信号灯(编号为0)的初值置为1。
一个父子进程利用共享内存段作为输入输出缓冲的例子 (p322) • 父进程一次读入一个字符串,将其存放在共享内存中,子进程从共享内存中取出数据,输出打印。 • 为了使父子进程之间达到同步,生成两个信号灯,采用传统的Wait、Signal操作思想,父子进程分别对它们进行信号灯操作。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #define SHMKEY 18001 /* 共享内存关键字 */ #define SIZE 1024 /* 共享内存长度 */ #define SEMKEY1 19001 /* 信号灯组1关键字 */ #define SEMKEY2 19002 /* 信号灯组2关键字 */ extern int creatsem(); extern void Wait(),Signal(),syserr();
main() { char *segaddr; int segid,sid1,sid2; /* 创建共享内存段 */ if((segid=shmget(SHMKEY,SIZE, IPC_CREAT|0666))==-1) syserr("shmget"); /* 将共享内存映射到进程数据空间 */ segaddr=shmat(segid,0,0); sid1=creatsem(SEMKEY1); /* 创建两个信号灯,初值为1 */ sid2=creatsem(SEMKEY2); Wait(sid2); /* 置信号灯2值为0,表示缓冲区空 */
if(!fork()) while(1){ /* 子进程,接收和输出 */ Wait(sid2); printf("Received from Parent: %s\n", segaddr); Signal(sid1); } while(1) { /* 父进程,输入和存储 */ Wait(sid1); scanf("%s",segaddr); Signal(sid2); } }
14.1文件系统程序设计 14.1.1获取文件的状态 在C语言程序设计中,有时需要获得有关文件的类型、大小、文件主及时间信息,这可通过系统调用stat和fstat来获取。 include <sys/types.h> include < sys/stat.h> int stat(pathname,sbuf) char pathname; int fstat(fd,sbuf) int fd; struct stat *sbuf; stat和fstat都是从一个文件的i节点获得有关状态信息的。stat是根据参数pathname给出的文件路径名,通过搜索目录项结构来获取文件的外存i节点,fstat是根据参数fd给出的打开文件描述符,通过打开文件结构获取内存i节点。
stat结构定义如下: struct stat { dev_t st_dev; /* i节点所在设备号*/ ino_t st_ino; /* i节点号(ushort) */ ushort st_mode; /* 文件模式 */ short st_nlink; /* 文件链接数(short)*/ ushort st_uid; /* 文件主用户标识符 */ ushort st_gid; /* 文件用户组标识符 */ dev_t st_rdev; /* 针对设备特别文件的设备号*/ off_t st_size; /* 文件的当前大小,特别文件为0*/ time_t st_atime; /* 文件存取时间(long)*/ time_t st_mtime; /* 文件修改时间(long)*/ time_t st_ctime; /* 文件的创建时间*/ } • 用fstat可以通过打开的无名管道文件描述符,访问“隐藏”的i节点,获取其状态信息,而stat调用则无能为力。
14.1.2搜索目录树 有时,用户需要在一棵目录树的范围内对文件和目录执行某些操作。例行程序ftw能从指定的目录开始扫描目录树,并对找到的每一个目录项,调用用户定义的函数。ftw函数的格式如下: #include <ftw.h> int ftw(path,func,depth) char *path; /* 指向目录路径名 */ int func(); /* 用户定义的处理函数 */ int depth; /* 即可同时打开的文件个数 */
用户定义的函数的格式: int func(name,statptr,type) char *name; /* 存放ftw找到的目标名 */ struct stat *statptr; /* 指向stat结构指针, ftw在该结构中存放目标的状态信息 */ int type; /* ftw指示目标的类型 和执行状态*/ { /* body of function */ }
参数type的目标类型 参数type的目标类型在ftw.h中定义,类型取值为: FTW_F 目标是文件 FTW_D 目标是目录 FTW_DNR 目标是不能读的目录 FTW_NS 目标不能被stat成功地执行 如果目标是不能读的目录,那么此目录的所有下级也不能处理。对于stat不能成功执行的目标,那么传送给用户的stat结构中的内容是无效的。 在调用ftw中,如用户定义的函数返回非0值,ftw就中止扫描,并把此时用户函数的返回值作为ftw的返回值。如ftw在执行中出错,会使ftw返回 -1,同时在errno中设置出错代码。
下面的程序使用ftw及dspstatus函数输出一棵目录树中所有带有路径的目录或文件名,对于文件还显示文件的类型、i节点号及文件链接数,对于特别文件还显示主次设备号。用户也可以在自定义函数中执行其他类型的操作。下面的程序使用ftw及dspstatus函数输出一棵目录树中所有带有路径的目录或文件名,对于文件还显示文件的类型、i节点号及文件链接数,对于特别文件还显示主次设备号。用户也可以在自定义函数中执行其他类型的操作。 • 主程序从参数中获得一个作为目录树扫描起点的路径名,该参数的缺省值为当前目录。
#incude <sys/types.h> #incude <sys/stat.h> #incude <ftw.h> int dspstatus(name,statptr,type) char *name; struct stat *statptr; int type; { switch(type){ case FTW_DNR: printf("-30s\t: Dir can’t read\n",name); case FTW_NS: /* 执行失败 */ return(0); default: /* 正常状态 */ break; } printf("-30s\t:",name);
switch(statprt->st_mode & S_IFMT){ case S_IFDIR: /* 目录 */ printf("Directory\n"); break; case S_IFBLK: /* 块特别文件 */ printf("Block special file\n"); printf("Device number: %d:%d\n", (statprt->st_rdev>>8)&0377, statprt->st_rdev&0377); break; …….. } printf("Inode: %d\t Links: %d\n", statptr->st_ino,statprr->st_nlink); } ……..
main(argc,argv) int argc; char **argv; { int dspstatus(); if(argc<2) ftw(".",dspstatus,2); else ftw(argv[1],dspstatus,2); exit(0); }
14.2用文件的系统调用实现进程通信 14.2.1利用文件的系统调用实现信号灯 • 可以用creat系统调用生成一个新文件,但如该文件原已存在,则调用进程要对其有写权限才能将该文件的长度截为零。利用这一特性可以将一个预先约定好的文件作为信号灯。 • 当若干进程要访问临界资源时,规定遵循以下协议:先试图创建同一个没有写权限的文件,这样只有一个进程才能获得成功,允许其进入临界区,其他进程的creat操作将失败(返回值为-1)。 • 进程从临界区退出时就删除这个作为信号灯的文件,这样正在等待中的进程的一个就能成功地创建它。
14.2.2利用管道实现进程间通信 用C语言实现含有管道符的UNIX复合命令 who | wc –l
生成管道 fork() fork() 关闭管道 等待子进程终止 使标准输出成为管道的 写端 使标准输入成为管道的 读端 who pipe文件 wc - l 父进程 子进程2 子进程1 图象改换 图象改换
who_wc() /* who | wc */ { int pfd[2]; if(pipe(pfd)==-1) syserr("pipe"); switch(fork()){ case -1: syserr("fork"); case 0: /* 子进程1 */ if(close(1)==-1) syserr("close"); if(dup(pfd[1])!=1){ fprintf(stderr,"ERROR: dup\n"); exit(1); }
if(close(pfd[0])==-1||close(pfd[1])==-1) syserr("close2"); execlp("who","who",NULL); } switch(fork()){ case 0: /*子进程2 */ if(close(0)==-1) syserr("close3"); if(dup(pfd[0])!=0) fprintf(stderr,"ERROR: dup2\n"); if(close(pfd[0])==-1||close(pfd[1])==-1) syserr("close4"); execlp("wc","wc","-l",NULL); syserr("execlp2"); } if(close(pfd[0])==-1||close(pfd[1])==-1) syserr(“close5”); /* 父进程 */ while(wait(NULL)!=-1) ; }
系统调用int dup(int fd)复制一个已存在的文件标识字,返回同一个文件或管道的新的文件标识字,该文件标识字是当前可用的最小文件标识字。两个文件标识字共享一个文件读写指针。 • 利用dup系统调用使用编号最小的可用文件标识字的规则,可以使对任何文件或管道的读写与所希望的文件标识字联系起来,如使用fd为0的标准输入与管道读相联系,fd为1的标准输出与管道写相联系,这就是管道命令的运行机制。 • 如将文件的输入/输出与标准输入/输出相联系,这就是I/O重定向的运行机制。
为了将前一个命令的标准输出与一个管道的写端相连接,后一个命令的标准输入与同一个管道的读端相连接,可以先关闭文字标识字0且用dup复制读管道的文件标识字,由于0是最小的文件标识字,且又刚刚使它处于“空闲”可用的状态,故dup将返回标识字为0的管道的读端。为了将前一个命令的标准输出与一个管道的写端相连接,后一个命令的标准输入与同一个管道的读端相连接,可以先关闭文字标识字0且用dup复制读管道的文件标识字,由于0是最小的文件标识字,且又刚刚使它处于“空闲”可用的状态,故dup将返回标识字为0的管道的读端。 • 同样也可使标识字1成为管道的写端。 • 接下来如果再关闭管道文件的原先两个文件标识字,就构成标识字为0和1的管道的读写端。 • 由于子进程和通过exec命令执行的进程继承了父进程的所有文件标识字和打开文件结构,故用这种方法可以建立管道命令符左右两边的命令输出和输入的联系。
父进程首先生成一个管道文件,再产生第一个子进程。父进程首先生成一个管道文件,再产生第一个子进程。 • 子进程继承了父进程的标准文件标识字和管道文件标识字,接着关闭标准输出,复制写管道文件标识字,其值为1。然后子进程关闭了从父进程继承来的管道文件的原两个标识字,使得该管道文件的写端仅与子进程的标准输出相连接。接下来子进程改换图像,执行UNIX命令who。 • 在who命令的执行期间,继承了第一个子进程的全部文件标识字和打开文件及管道文件状态,who命令的标准输出就源源不断地写入管道文件中。 • 为了建立第二个命令的管道读端,原父进程又产生了第二个子进程,类似地,将标准输入与管道文件的读端相连接,并执行UNIX命令wc,使该命令从管道的读端接收数据,从而建立了who和wc两个命令的读写联系。