[C程序设计语言]第五部分

 
 

UNIX系统接口

文件描述符


UNIX操作系统中,所有的外围设备(包括键盘和显示器)都被看作是文件系统中的文件,因此,所有的输入/输出都要通过读文件或写文件完成。
    因为大多数的输入/输出是通过键盘和显示器来实现的,为了方便起见,UNIX对此做了特别的安排。当命令解释程序(即“shell”)运行一个程序的时候,它将打开3个文件,对应的文件描述符分别为0, 1, 2,依次表示标准输入,标准输出和标准错误。如果程序从文件0中读,对12进行写,就可以进行输/输出而不必关心打开文件的问题。
    程序的使用者可通过<>重定向程序的I/O:
    prog <输入文件名 >输出文件名
这种情况下,shell把文件描述符0l的默认赋值改变为指定的文件。通常,文件描述符2仍与显示器相关联,这样,出错信息会输出到显示器上。在任何悄况下,文件赋值的改变都不是程序完成的,而是由shell完成的。只要程序使用文件0作为输入,文件l2作为输出,它就不会知程序的输入从哪里来,并输出到哪里去。

底层I/O readwrite


    输入与输出是通过现readwrite系统调用实现的。在C语言程序中,可以通过函数readwrite访问这两个系统调用。这两个函数中,第一个参数是文件描述符,第二个参数是程序中存放读和写的数据的字符数组,第三个参数是要传输的字节数:
int n_read = read(int fd, char *buf, int n);
int n_written = write(int fd, char *buf, int n);
每个调用返回实际传输(读或写)的字节数。在读文件时,函数的返回值可能会小于请求的字节数,如果返回值为0,则表示已到达文件的结尾;如果返回值为-1,则表示发生了某种错误。在写文件时,返回值是实际写入的字节数,如果返回值与请求写入的字节数不相等,则说明发生了错误
    在一次调用中,读出或写入的数据的字节数可以为任意大小。最常用的值为1,即每次读出或写入1个字符(无缓冲),或是类似于1024~4096这样的与外围设备的物理块大小相应的值。用更大的值调用该函数可以获得更高的效率,因为系统调用的次数减少了。
#include"syscalls.h"
main() /*将输入复制到输出*/
{
    char buf[BUFSIZ];
    int n;
    while ((n = read(0, buf, BUFSIZ)) > 0)
       write(1, buf, n);
    return 0;
}
本章中的程序都将包含该头文件syscalls.h不过,该文件的名字不是标准的。参数BUFSIZ已经在syscalls.h头文件中定义了。对于所使用的操作系统来说,该值是一个较合适的数值。如果文件大小不是BUFSIZ的倍数,则对read的某次调用会返回一个较小的字节数,write再按这个字节数写。
 
构造类似于getchar高级函数: 从标准输入读入一个字符来实现无缓冲输入(第一个版本)
#include"syscalls.h"
int getchar(void) {
    char c;
    return (read(0, &c, 1) == 1) ? (unsignedchar) c : EOF;
}
第二个版本:一次读入一组字符,但每次只输出一个字符
#include"syscalls.h"
int getchar(void) {
    staticchar buf[BUFSIZ];
    staticchar *bufp = buf;
    staticint n = 0;
    if (n == 0) { /* buffer is empty */
       n = read(0, buf, sizeof buf);
       bufp = buf;
    }
    return (n >= 0) ? (unsignedchar) *bufp++ : EOF;
}
如果要在包含头文件始<stdio.h>的情况下编译这些版本的getchar函数,就有必要用#-undef预处理指令取消名字getchar的宏定义,因为在头文件中,getchar是以宏方式实现的

open, creat, closeunlink


    除了默认的标准输入、标准输出和标准错误文件外,其它文件都必须在读或写之前显式地打开。系统调用opencreat用于实现该功能。
    open与前面讨论的fopen相似,不同的是,前者返回一个文件描述符,它仅仅只是一个int类型的数值。而后者返回一个文件指针。如果发生错误,open将返回-l
#include <fcntl.h>
int fd;
int open(char *name, int flags, int perms);
fd = open(name, flags, perms);
fopen一样,参数name是一个包含文件名的字符串,第二个参数flags是一个int类型的值,它说明以何种方式打开文件,主要的几个值如下所示:
    O_RDONLY                只读方式打开文件
     O_WRONLY               只写方式打开文件
    O_RDWR          读写方式打开文件
 在这里讨论的open第三个参数始终为0
 
使用create创建或覆盖已存在的旧文件:
int creat(char *name, int perms);
fd = creat(name, perms);
如果creat成功地创建了文件,它将返回一个文件描述符,否则返回-1。如果此文件已存在,creat将把该文件的长度截断为0,从而丢弃原先己有的内容使用creat创建一个已存在的文件不会导致错误
如果要创建的文件不存在,则creat用参数pems指定的权限创建文件。在UNIX文件系统中,每个文件对应一个9比特的权限信息,它们分别控制文件的所有者、所有者组和其他成员对文件的读、写和执行访问。因此,通过一个3位的八进制数就可方便地说明不同的权限,例如,0755(八进制)说明文件的所有者可以对它进行读、写和执行操作,而所有者组和其他成员只能进行读和执行操作。
 
下面通过一个简化UNIX程序cp说明creat的用法,该程序将一个文件复制到另一个文件。目标文件的权限不是通过复制获得的,而是重新定义的:
#include<stdio.h>
#include<fcntl.h>
#include<stdarg.h>
#include"syscalls.h"
#define PERMS 0666 /* RW for owner, group, others */
void error(char *, ...);
/* cp: copy f1 to f2 */
main(int argc, char *argv[]) {
    int f1, f2, n;
    char buf[BUFSIZ];
    if (argc != 3)
       error("Usage: cp from to");
    if ((f1 = open(argv[1], O_RDONLY, 0)) == 1)
       error("cp: can't open %s", argv[1]);
    if ((f2 = creat(argv[2], PERMS)) == 1)
       error("cp: can't create %s, mode %03o", argv[2], PERMS);
    while ((n = read(f1, buf, BUFSIZ)) > 0)
       if (write(f2, buf, n) != n)
           error("cp: write error on file %s", argv[2]);
    return 0;
}
void error(char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    fprintf(stderr, "error: ");
    vprintf(stderr, fmt, args);
    fprintf(stderr, "\n");
    va_end(args);
    exit(1);
}
该程序创建的输出文件具有固定的权限0666,也可使用stat系统调用,可以获得一个已存在文件的模式,并将此模式赋值给它的副本。标准库函数vprintf数与printf函数类似,所不同的是,它用一个参数取代了变长参数表,因此参数通过调用va_start宏进行初始化。同样,vfprintfvsprintf函数分别与fprintfsprintf函数类似。
 
函数close (int fd)用来断开文件描述符和已打开文件之间的连接,并释放此文件描述符,以供其它文件使用。close函数与标准库中的fclose函数相对应,但它不需要清除(flush)缓冲区。
 
函数unlink(char *name)将文件name从文件系统中删除,它对应于标准库函数remove

随机访问-lseek


系统调用lseek可以在文件中任意移动位置而不实际读写任何数据
    long lseek(int fd, long offset, int origin);
文件描述符为fd的文件的当前位置设置为offset,其中,offset是相对于orgin指定的位置而言的。随后进行的读写操作将从此位置开始,origin的值可以为012,分别用于指定offset从文件开始、从当前位置或从文件结束处开始算起。例如,为了向一个文件的尾部添加内容(在UNIX shell程序中使用重定向符>>或在系统调用fopen中使用参数“a”):
    lseek(fd, 0L, 2);
 
#include"syscalls.h"
/*get: read n bytes from position pos 从文件的任意位置读入任意数目的字节,它返回读入的字节数,若发生错误返回-1*/
int get(int fd, long pos, char *buf, int n) {
    if (lseek(fd, pos, 0) >= 0) /* get to pos 先移动到指定的位置*/
       returnread(fd, buf, n); /* 再读取*/
    else
       return 1;
}
 
lseek系统调用返回long类型的值,此值表示文件的新位置,若发生错误,则返回-1。标准库函数fseek与系统调用lseek类似,不同的是,前者第一个参数为FILE *类型,并且发生错误时返回一个非0值。

实例-fopengetc函数的实现


标准库中的文件不是通过文件描述符描述的,而是使用文件指针描述的文件指针是一个指向包含文件各种信息的结构的指针,该结构包含下列内容:一个指向缓冲区的指针,通过它可以一次读入文件的一大块内容;一个记录缓冲区中剩余的字符数的计数器;一个指向缓冲区中下一个字符的指针;文件描述符;描述读/写模式的标志;描述错误状态的标志等。
——相关宏定义——
#define NULL 0
#define EOF (1)
#define BUFSIZ 1024
#define OPEN_MAX 20 /* 一次允许打开的最大文件数 */
typedefstruct _iobuf {
    intcnt; /* 剩余字符数 */
    char *ptr; /* 下一个字符的位置 */
    char *base; /* 缓冲区的位置 */
    intflag; /* 文件访问模式与出错标示 */
    intfd; /* 文件描述符 */
} FILE;
 
externFILE _iob[OPEN_MAX]; //用来存储打开的文件结构数据
#define stdin (&_iob[0]) //第一个用作标准输入
#define stdout (&_iob[1]) //第二个用作标准输出
#define stderr (&_iob[2]) //第三个用作错误输出
enum _flags {
    /*  使用了5位来标示文件的访问方式与文件访问时出错标示: 00 011 111
       以位来标示便于使用位运算符&来判断文件的打开后所处的状态,如判断打开
       文件是否出错:(fp -> flag & (_EOF | _ERR)) != 0 */
    _READ = 01, /* 以读方式打开文件 */
    _WRITE = 02, /* 以写方式打开文件 */
    _UNBUF = 04, /* 不对文件进行缓冲 */
    _EOF = 010, /* 已到文件尾 */
    _ERR = 020 /* 该文件发生错误 */
};
 
int _fillbuf(FILE *);
int _flushbuf(int, FILE *);
 
#define feof(p) ((p)->flag & _EOF) != 0)
#define ferror(p) ((p)->flag & _ERR) != 0)
#define fileno(p) ((p)->fd)
/* 先将计数器cnt减一,再将指针移到下一个位置,然后返回字符,
* 如果小于0,则就填允缓冲区,重新初始化结构的内容,并返回
* 一个字符*/
#define getc(p) (--(p)->cnt >= 0 \
       ? (unsignedchar) *(p)->ptr++ : _fillbuf(p))
/* 缓冲区满后刷新缓冲到文件中*/
#define putc(x,p) (--(p)->cnt >= 0 \
       ? *(p)->ptr++ = (x) : _flushbuf((x),p))
#define getchar() getc(stdin)
#define putcher(x) putc((x), stdout)
 
——fopen实现——
#include<fcntl.h>
#include"syscalls.h"
#define PERMS 0666 /* RW for owner, group, others */
FILE *fopen(char *name, char *mode) {
    int fd;
    FILE *fp;
    if (*mode != 'r' && *mode != 'w' && *mode != 'a')
       return NULL;
    for (fp = _iob; fp < _iob + OPEN_MAX; fp++)
       if ((fp -> flag & (_READ | _WRITE)) == 0)
           break; /* 寻找一个空位置,即让fp指向_iob数据还未使用的元素
,如果结构数据未赋值,则_READ | _WRITE位所在的
位为0,则(_READ | _WRITE)就为0 */
    if (fp >= _iob + OPEN_MAX) /* 最大文件个数超限 */
       return NULL;
    if (*mode == 'w')
       fd = creat(name, PERMS);
    elseif (*mode == 'a') {
       //如果以写的方式打开文件后返回的文件描述符为1(标准输出),
       //则重新创建一个文件
       if ((fd = open(name, O_WRONLY, 0)) == 1)
           fd = creat(name, PERMS);
       lseek(fd, 0L, 2);//定位到文件结尾
    } else
       fd = open(name, O_RDONLY, 0);
    if (fd == -1)
       /* 不能访问文件 */
       return NULL;
    /* 正常打开文件后,将对应的文件结构数据存储到_iob结构数组中相应空闲元素的位置中,
  _iob结构数组主要的是一个全局的,用来记录当前程序已打开文件,这样就可以限制打开的
最多文件数*/
    fp -> fd = fd;
    fp -> cnt = 0;
    fp -> base = NULL;
    fp -> flag = (*mode == 'r') ? _READ : _WRITE;
    return fp;
}
 
——缓冲函数的实现——
#include"syscalls.h"
/* _fillbuf: 分配并填允缓冲区 */
int _fillbuf(FILE *fp) {
    int bufsize;
    if ((fp -> flag & (_READ | _EOF | _ERR)) != _READ)
       return EOF;
    bufsize = (fp -> flag & _UNBUF) ? 1 : BUFSIZ;
    if (fp -> base == NULL) /* 还未分配缓冲区时 */
       if ((fp -> base = (char *) malloc(bufsize)) == NULL)
           return EOF; /* 不能分配缓冲区 */
    fp -> ptr = fp -> base;
    fp -> cnt = read(fp -> fd, fp -> ptr, bufsize);
    if (fp -> cnt < 0) {
       if (fp -> cnt == 1)
           fp -> flag |= _EOF;
       else
           fp -> flag |= _ERR;
       fp -> cnt = 0;
       return EOF;
    }
    return (unsignedchar) *fp -> ptr++;
}
 
/* 最后我们还必须定义和初始化数组_iob中的stdin, stdout, stderr*/
FILE _iob[OPEN_MAX] = {
    {   0, (char *) 0, (char *) 0, _READ, 0},
    {   0, (char *) 0, (char *) 0, _WRITE, 1},
    {   0, (char *) 0, (char *) 0, _WRITE | _UNBUF, 2}/* 标准错误输出不用缓冲的 */
};
 

性能忠告


在内存紧张和要求运行速度的情况下,最好使用数值范围比较小的整数数据类型。
 
在声明时就赋值可能提高程序运行时间。
 
c +=5 c = c + 5要快,原因是前者中c只分析一次,而后者中c要分析两次。
 
在使用运算符&&的表达式中,把最可能为假的条件放在最左边。在使用运算符||的表达式中,把最可能为真的条件放在最左边,这样能减少程序的执行时间。

可移植性忠告


使用符号常量EOF而不是使用-1可使用程序具有更好的可移植性。ANSI标准规定EOF是一个负整数,但不是必须为-1。因此,在不同的系统中,EOF可能有不同的值。
 
因为int类型的整数值在不同的系统中具有不同的范围,所以如果要处理 -32768 ~ +32767范围之外的整数值并且要能够使用程序在不同的计算机系统中运行,使用long数据类型。
 

函数原型


函数原型告诉编译器函数返回的数据类型,函数所要接收的参数个数,参数类型和参数顺序,编译器用函数原型校验函数调用。
int maxinum(int, int, int);
这里省略了参数名,也可以带上参数名
 
函数原型另一个重要特点是强制转换参数类型,如标准库函数sqrt函数原型指定了参数为double类型,但在调用时可以传递整型,这是因为编译器根据函数原型,调用前将整型转换为double型了。如果不遵循C语言的类型“提升规则”,参数类型转换可能会导致不正确的结果。
 
 
posted @ 2015-01-28 21:34  江正军  阅读(654)  评论(0编辑  收藏  举报