The C Programming Language——UNIX系统接口

概述

UNIX操作系统通过一系列的系统调用提供服务,这些系统调用实际上是操作系统内的函数,它们可以被用户程序调用

本章将介绍如何在C语言程序中(在其他语言的程序中也可使用,只是调用方式会有些许不同而已)使用一些重要的系统调用

我们经常需要借助于系统调用以获得最高的效率,或者访问标准库中没有的某些功能

不同系统中的代码具有相似性,只是一些细节上有区别而已

ANSI C标准函数库是以UNIX系统为基础建立起来的

在任何特定的系统中,标准库函数的实现必须通过宿主系统提供的功能来实现

任何操作系统所提供的C标准函数库的接口都是一样的(有ANSI C定义)


1、文件描述符

在UNIX系统中,所有的外围设备(包括键盘和显示器)都被看作是文件系统中的文件,因此,所有的输入/输出都要通过读文件或写文件完成。也就是说,通过一个单一的接口就可以处理外围设备和程序之间的所有通信

通常情况下,在读或写文件之前,必须先将这个意图通知系统,该过程称为打开文件。如果是写一个文件,则可能需要先创建该文件,也可能需要丢弃该文件中原先已存在的内容。系统检查你的权利(文件是否存在和权限问题),如果一切正常,操作系统将向程序返回一个小的非负整数,该整数称为文件描述符

任何时候对文件的输入/输出都是通过文件描述符识别文件,而不是通过文件名识别文件(文件描述符类似于标准库中的文件指针或MS-DOS中的文件句柄

系统负责维护已打开文件的所有信息,用户程序只能通过文件描述符引用文件

因为大多数的输入/输出是通过键盘和显示器来实现的,为了方便起见,UNIX对此做了特别的安排。当shell运行一个程序的时候,它将打开3个文件,对应的文件描述符分别为0、1、2,依次表示标准输入、标准输出、标准错误

默认情况下,标准输入默认关联键盘;标准输出和标准错误默认关联显示器。这种关联是可以改变的

程序的使用者可通过<和>重定向程序的I/O:prog <输入文件名>输出文件名

上述这种情况,shell把文件描述符0和1的默认赋值(默认关联的文件)改变为指定的文件了。通常,文件描述符2仍与显示器相关联,这样,出错信息会输出到显示器上(与管道相关的输入/输出也有类似的特性)

在任何情况下,文件赋值的改变都不是由用户程序完成的,而是又shell完成的

只要程序使用文件描述符0作为输入文件描述符1和2作为输出,它(程序)就不会知道程序的输入到底从哪里来,并到底输出到哪里去


2、低级I/O——read和write

输入与输出是通过read和write系统调用实现的。在C语言程序中,可以通过函数read和write访问这两个系统调用(通过函数访问系统调用

//第一个参数是文件描述符,第二个参数是程序中存放读或写的数据的字符数组,第三个是要传输的字节数
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这样的与外围设备的物理块大小相应的值。用更大的值调用该函数可以获得更高的效率,因为系统调用的次数减少了

输入/输出可以重定向到任何文件或设备

//用read和write构造getchar函数

//无缓冲的单字符输入
int getchar(void)
{
    char c;

    return (read(0, &c, 1) == 1) ? (unsigned char)c : EOF;
}

//带有缓冲区的版本,一次读入一组字符,但每次只输出一个字符
int getchar(void)
{
    static char buf[BUFSIZ];
    static char *bufp = buf;
    static int n = 0;//只会进行一次初始化

    if(n == 0)
    {
        n = read(0, buf, sizeof(buf));//sizeof对于数组名运算的效果
        bufp = buf;
    }
    return (--n >= 0) ? (unsigned char)(*bufp++) : EOF;
}
//在返回语句中将c转换为unsigned char类型可以消除符号扩展问题

如果要在包含了<stdio.h>的情况下编译这些版本的getchar函数,就有必要用#undef预处理指令取消名词getchar的宏定义,因为在头文件中,getchar是以宏方式实现的


3、open、creat、close和unlink

除了默认的标准输入、标准输出和标准错误文件外,其他文件都必须在读或写之前显式地打开系统调用open和creat用于实现该功能

#include <fcntl.h>

int open(char *name, int flags, int perms);

open函数如果发生错误将返回-1

flags说明以何种方式打开文件:

  • O_RDONLY:以只读方式打开文件
  • O_WRONLY:以只读方式打开文件
  • O_RDWR:以读写方式打开文件

System V UNIX系统中,这些常量在头文件<fcntl.h>中定义,而在Berkeley(BSD)版本中则在<sys/file.h>中定义

如果用open打开一个不存在的文件,则将导致错误

//使用creat系统调用创建新文件或覆盖已有的旧文件
int creat(char *name, int perms);

如果creat成功地创建了文件,它将返回一个文件描述符,否则返回-1。如果此文件已存在,creat将把该文件的长度截断为0,从而丢弃原先已有的内容。使用creat创建一个已存在的文件不会导致错误

如果要创建的文件不存在,则creat用参数perms指定的权限创建文件。

在UNIX文件系统中,每个文件对应一个9比特的权限信息,它们分别控制文件的所有者所有者组其他成员对文件的读、写和执行访问。因此,通过一个3位的八进制数就可方便地说明不同的权限

//简化的UNIX程序cp,目标文件的权限不是通过复制获得的,而是重新定义的。
//stat系统调用,可以获得一个已存在文件的模式,并将此模式赋值给它的副本
#include <stdio.h>
#include <fcntl.h>
#include <stdarg.h>
#inlcude "syscalls.h"

#define PERMS 0660

void error(char *, ...);

int 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: ");
    vfprintf(stderr, fmt, args); //这里很有意思
    fprintf(stderr, "\n");
    va_end(args);
    exit(1);
}

标准库函数vprintf函数与printf函数类似,所不同的是,它用一个参数取代了变长参数表,且此参数通过调用va_start宏进行初始化(看懂上述error函数的实现就理解了)

vfprintf和vsprintf函数分别与fprintf和sprintf函数类似

一个程序同时打开的文件数是有限制的(通常为20)。相应的,如果一个程序需要同时处理许多文件,那么它必须重用文件描述符函数close(int fd)用来断开文件描述符与已打开文件之间的连接,并释放此文件描述符,以供其他文件使用。该函数与标准库中的fclose函数相对应,但它不需要刷新缓冲区(是因为最底层的系统调用没有缓冲区吗??????

如果程序通过exit函数退出或从主程序中返回,所有打开的文件将被关闭(只要程序是正常退出的,所有打开的文件都将被关闭)

函数unlink(char *name)将文件name从文件系统中删除,它对应于标准库函数remove


4、随机访问——lseek

输入/输出通常是顺序进行的:每次调用read和write进行读写的位置紧跟在前一次操作的位置之后

有时候需要以任意顺序访问文件,系统调用lseek可以在文件中任意移动位置而不实际读写任何数据(和任意移动光标一个意思)
long lseek(int fd, long offset, int origin)将文件描述为fd的文件的当前位置设置为offset,其中,offset是相对于origin指定的位置而言的。随后进行的读写操作将从此位置开始。origin的值可以为0、1和2,分别用于指定offset从文件开始处从当前位置从文件结束处开始算起(offset中保存数字的单位是什么?????)

使用lseek系统调用时,可以将文件视为一个大数组,其代价是访问速度会慢一些。

lseek系统调用返回一个long类型的值,此值表示文件的新位置(相对于谁?????),若发生错误,则返回-1。标准库函数fseek与系统调用lseek类似


5、实例——fopen和getc函数的实现

文件指针是一个指向包含文件各种信息的结构的指针,该结构包含下列内容:一个指向缓冲区的指针,通过它可以一次读入文件的一大块内容;一个记录缓冲区中剩余的字符数的计数器;一个指向缓冲区中下一个字符的指针;文件描述符;描述读/写模式的标志;描述错误状态的标志等

<stdio.h>中,只供标准库中其他函数所使用的名字以下划线开始,因此一般不会与用户程序中的名字冲突。所有的标准库函数都遵循该约定

fopen函数的主要功能是打开文件,定位到合适的位置,设置标志位以指示相应的状态。它不分配任何缓冲区空间,缓冲区的分配是在第一次读文件时由函数_fillbuf完成的

//fopen函数的实现
#include <fcntl.h>
#include "syscalls.h"

#define PERMS 0666

FILE *fopen(char *name, char *mode)
{
    int fd;
    FILE *fp;

    if(*mode != 'r' && *mode != 'w' && *mode != 'a')
        return NULL;
    for(fp = _iob; fb < _iob + OPEN_MAX; fp++)
        if((fp->flag & (_READ | _WRITE)) == 0)
            break;
    if(fp >= _iob + OPEN_MAX)
        return NULL;

    if(*mode == 'w')
        fd = creat(name, PERMS);
    else if(*mode == 'a')
    {
        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;
    fp->fd = fd;
    fp->cnt = 0;
    fp->base = NULL;
    fp->flag = (*mode == 'r') ? _READ : _WRITE;
    return fp;
}

对于某一特定的文件,第一次调用getc函数时计数值(cnt)为0,这样就必须调用一次函数_fillbuf。如果_fillbuf发现文件不是以读方式打开的,它将立即返回EOF;否则,它将试图分配一个缓冲区(如果读操作是以缓冲方式进行的话)。建立缓冲区后,_fillbuf调用read填充此缓冲区,设置计数值和指针,并返回缓冲区中的第一个字符。随后进行的_fillbuf调用会发现缓冲区已经分配

//_fillbuf函数的实现(分配并填充输入缓冲区)
#include "syscalls.h"

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 (unsigned char)*fp->ptr++;
}

//每个shell所执行的程序默认的设置
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}
};

stderr执行无缓冲方式的写操作

理解整个流程的执行逻辑


6、实例——目录列表

常常还需要对文件系统执行另一种操作,以获得文件的有关信息,而不是读取文件的具体内容

在UNIX系统中,目录也是一种文件,目录包含了一个文件名列表和一些指示文件位置的信息。“位置”是一个指向其他表(即i结点表)的索引。文件的i结点是存放除文件名以外的所有文件信息的地方目录项通常包含两个条目:文件名和i结点编号

无论实现方式是否同具体的系统有关,我们需要提供一种与系统无关的访问文件信息的途径

//fsize程序的实现
#include <stdio.h>
#include <string.h>
#include "syscalls.h"
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "dirent.h"

#define NAME_MAX 14

typedef struct{ //可移植的目录项
    long ino;
    char name[NAME_MAX + 1];
} Dirent;

//目录包含一个文件名列表和一些指示文件位置的信息。“位置”是一个指向其他表(即i结点表)的索引。
//以下结构的定义看不出一个目录是如何包含多个文件的??????
typedef struct{ //目录文件
    int fd; //目录的文件描述符
    Dirent d;
} DIR;

DIR *opendir(char *dirname);
Dirent *readdir(DIR *dfd);
void closedir(DIR *dfd);

void fsize(char *);

int main(int argc, char ** argv)
{
    if(argc == 1)
        fsize("."); //处理当前目录
    else
        while(--argc > 0)
            fsize(*++argv);
    return 0;
}

int stat(char *, struct stat *);
void dirwalk(char *, void(*fcn)(char *));

void fsize(char *name)
{
    struct stat stbuf;

    if(stat(name, &stbuf) == -1)
    {
        fprintf(stderr, "fsize: can't access %s\n", name);
        return;
    }
    if((stbuf.st_mode & S_IFMT) == S_IFDIR)
        dirwalk(name, fsize);
    printf("%8ld %s\n", stbuf.st_size, name);
}

#define MAX_PATH 1024

void dirwalk(char *dir, void(*fcn)(char *))
{
    char name[MAX_PATH];
    Dirent *dp;
    DIR *dfd; //该结构包含上面的那个结构

    if((dfd = opendir(dir)) == NULL)
    {
        fprintf(stderr, "dirwalk: can't open %s\n", dir);
        return;
    }
    while((dp = readdir(dfd)) != NULL) //这里实现循环遍历目录中的所有文件(包括目录)
    {
        if(strcmp(dp->name, ".") == 0 || strcmp(dp->name, "..") == 0)
            continue;
        if(strlen(dir) + strlen(dp->name) + 2 > sizeof(name))
            fprintf(stderr, "dirwalk: name %s/%s too long\n", dir, dp->name);
        else
        {
            sprintf(name, "%s/%s", dir, dp->name);
            (*fcn)(name);
        }
    }
    closedir(dfd);
}

DIR *opendir(char *dirname)
{
    int fd;
    struct stat stbuf;
    DIR *dp;

    if((fd = open(dirname, O_RDONLY, 0)) == -1 
    || fstat(fd, &stbuf) == -1 
    || (stbuf.st_mode & S_IFMT) != S_IFDIR
    || (dp = (DIR *)malloc(sizeof(DIR))) == NULL)
        return NULL;
    dp->fd = fd;
    return dp;
}

void closedir(DIR *dp)
{
    if(dp)
    {
        close(dp->fd);
        free(dp);
    }
}

#include <sys/dir.h>

Dirent *readdir(DIR *dp)
{
    struct direct dirbuf; //不可移植的目录项
    static Dirent d; //可移植的目录项

    while(read(dp->fd, (char *) &dirbuf, sizeof(dirbuf)) == sizeof(dirbuf))
    {
        if(dirbuf.d_ino == 0)
            continue;
        d.ino = dirbuf.d_ino;
        strncpy(d.name, dirbuf.d_name, DIRSIZ);
        d.name[DIRSIZ] = '\0';
        return &d;
    }
    return NULL;
}

注意如何使用文件<sys/stat.h>中的标志名S_IFMTS_IFDIR来判定文件是不是一个目录

函数dirwalk是一个通用函数,它对目录中的每个文件都调用函数fcn一次。它首先打开目录,循环遍历其中的每个文件,并对每个文件调用该函数,然后关闭目录返回。因为fsize函数对每个目录都要调用dirwalk函数,所以这两个函数是相互递归调用

每次调用readdir都将返回一个指针,它指向下一个文件的信息。如果目录中已没有待处理的文件,该函数将返回NULL(遍历的关键在readdir函数中)

每个目录都包含自身“.”父目录“..”的项目,在处理时必须跳过它们,否则将导致无限循环

所有的系统类型可以在文件<sys/types.h>中找到

如果某个目录位置当前没有使用(因为删除了一个文件),则它的i结点编号为0,并跳过该位置

许多程序并不是系统程序,它们仅仅使用由操作系统维护的信息。对于这样的程序,很重要的一点是,信息的表示仅出现在标准头文件中,使用它们的程序只需要在文件中包含这些头文件即可,而不需要包含相应的声明(看不懂这句话???????)

为与系统相关的对象创建一个与系统无关的接口


7、实例——存储分配程序

本次要实现的malloc和free函数可以以任意次序调用来分配和释放动态内存。这些程序说明了通过一种与系统无关的方式编写与系统有关的代码时应考虑的问题,同时也展现了结构、联合和typedef的实际应用

malloc在必要时调用操作系统以获取更多的存储空间

malloc并不是从一个在编译时就确定的固定大小的数组中分配存储空间,而是在需要时向操作系统申请空间(动态存储

因为程序中的某些地方可能不通过malloc调用申请空间(即通过其他方式申请空间),所以,malloc管理的空间不一定是连续的(意思是整个是不连续的,局部是连续的)

由malloc管理的)空闲存储空间以空闲块链表的方式组织,每个块包含一个长度一个指向下一块的指针以及一个指向自身存储空间的指针。这些块按照存储地址的升序组织,最后一块(最高地址)指向第一块

malloc分配存储空间的算法有“首次适应”“最佳适应”两种

如果malloc在其管理的空闲块链表中找不到一个足够大的块,则向操作系统申请一个大块并加入到空闲块链表中(通过morecore函数

释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适的位置(按照地址升序插入)。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合成为一个更大的块,这样存储空间不会有太多的碎片。因为空闲块链表是以地址的递增顺序链接在一起的,所以很容易判断相邻的块是否空闲

有malloc函数返回的存储空间要满足将要保存的对象的对齐要求

每个特定的机器都有一个最受限的类型:如果最受限的类型可以存储在某个特定的地址中,则其他所有的类型也可以存放在此地址中

不同的机器具有不同的最受限的类型,一般为double或int或long类型

空闲块包含一个指向链表中下一个块的指针、一个块大小的记录和一个指向空闲空间本身的指针。位于块开始处的控制信息称为“头部”

为了简化块的对齐,所有块的大小都必须是头部大小的整数倍,且头部已正确地对齐。这是通过一个联合实现的,该联合包含所需的头部结构以及一个对齐要求最受限的类型的实例

在malloc函数中,请求的长度(以字符为单位)将被舍入,以保证它是头部大小的整数倍(只多不少)。实际分配的块将多包含一个单元,用于头部本身。实际分配的块的大小将被记录在头部的size字段中。malloc函数返回的指针将指向空闲空间,而不是块的头部。

用户可对获得的存储空间进行任何操作,但是,如果在分配的存储空间之外写入数据,则可能会破坏块链表

//malloc和free函数的一种实现
typedef long Align; //这里假设long为最受限的类型

union header{
    struct{
        union header *ptr;
        unsigned size;
    } s;
    Align x;
};
//Align字段永远不会被使用,它仅仅用于强制每个头部在最坏的情况下满足对齐要求
typedef union header Header;

static Header base;
static Header *freep = NULL;

void *malloc(unsigned nbytes)
{
    Header *p, *prevp;
    Header *morecore(unsigned);
    unsigned nunits;

    nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;
    if((prevp = freep) == NULL)
    {
        base.s.ptr = freep = prevp = &base;
        base.s.size = 0;
    }
    for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr)
    {
        if(p->s.size >= nunits)
        {
            if(p->s.size == nunits)
                prevp->s.ptr = p->s.ptr;
            else
            {
                p->s.size -= nunits;
                p += p->s.size;
                p->s.size = nunits;
            }
            freep = prevp;
            return (void *)(p + 1);
        }
        if(p == freep)
            if((p = morecore(nunits)) == NULL)
                return NULL;
    }
}

#define NALLOC 1024

static Header *morecore(unsigned nu)
{
    char *cp, *sbrk(int);
    Header *up;

    if(nu < NALLOC)
        nu = NALLOC;
    cp = sbrk(nu * sizeof(Header));
    if(cp == (char *)-1)
        return NULL;
    up = (Header *)cp;
    up->s.size = nu;
    free((void *)(up + 1));
    return freep;
}

void free(void *ap)
{
    Header *bp, *p;

    bp = (Header *)ap - 1;
    for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
        if(p >= p->s.ptr && (bp >p || bp < p->s.ptr))
            break;

    if(bp + bp->s.size == p->s.ptr)
    {
        bp->s.size += p->s.ptr->s.size;
        bp->s.ptr = p->s.ptr->s.ptr;
    }
    else
        bp->s.ptr = p->s.ptr;
    if(p + p->s.size == bp)
    {
        p->s.size += bp->s.size;
        p->s.ptr = bp->s.ptr;
    }
    else
        p->s.ptr = bp;
    freep = p;
}

其中的size字段是必需的,因为有malloc函数控制的块不一定是连续的,这样就不可能通过指针算术运算计算其大小(这句话没道理呀,因为空闲块是局部连续的)

任何情况下,当请求空闲空间时,都将搜索空闲块链表。搜索从上一次找到空闲块的地方开始。该策略可以保证链表是均匀的

每个空闲块的长度都是头部长度的整数倍

在任何情况下,返回给用户的指针都指向块内的空闲存储空间,即比指向头部的指针大一个单元

函数morecore用于向操作系统请求存储空间,其实现细节因系统的不同而不同。因为向系统请求存储空间是一个开销很大的操作,因此,我们不希望每次调用malloc函数时都执行该操作,基于这个考虑,morecore函数请求至少NALLOC个单元

强制类型转换使得指针之间的比较不会受不同机器中指针表示的不同的影响

这里假定,由sbrk调用返回的指向不同块的多个指针之间可以进行有意义的比较。ANSI C标准并没有保证这一点,它只允许指向同一个数组的指针之间的比较。因此,只有在一般指针间的比较操作有意义的机器上,该版本的malloc函数才能够移植

合并两个块的操作很简单,只需要设置指针指向正确的位置,并设置正确的块大小就可以了

类型的强制转换使得指针的转换是显式进行的,这样做甚至可以处理设计不够好的系统接口问题

malloc函数管理的内存空间和操作系统管理的内存空间不是一回事(因为malloc管理的块不够大时,还需要向操作系统申请更大的块)

猜你喜欢

转载自blog.csdn.net/weixin_39918693/article/details/80636045