系统调用

UNIX操作系统通过一系列系统调用提供服务,而这些系统调用实际上是操作系统内的函数,可以被用户程序调用
在UNIX系统中,经常会需要借助于系统调用以获得最高的效率,或访问标准库中没有的某些功能
尤其是,ANSI C标准函数库是以UNIX系统为基础建立起来的(比如fopen等文件操作函数是基于open等系统调用接口实现的)
此外,在任何特定的系统中,标准库函数的实现往往必须通过宿主系统提供的功能来实现
另一方面,许多程序并不是系统程序,而是仅仅使用由操作系统维护的信息
对于这样的程序,很重要的一点是信息的表示仅出现在标准头文件中,使用它们的程序只需要在文件中包含头文件即可,而不需要包含每个相应的声明
其次,在构建应用程序时为了可移植性,有可能需要为与系统相关的对象创建一个与系统无关的接口

文件描述符

在UNIX系统中,所有的外围设备(包括键盘和显示器)都被看作文件系统的文件,因而所有输入/输出都要通过读文件或写文件完成
即,在UNIX系统中通过单一的接口就可以处理外围设备和程序之间的所有通信
通常,读写文件之前必须将意图通知系统,该过程称为打开文件
如果是写一个文件,则可能需要先创建该文件,也可能需要丢弃该文件中原先已存在的内容
UNIX系统会检查该文件是否存在,以及用户是否有访问它的权限
如果一切正常,操作系统将向程序返回一个小的非负整数,该整数称为文件描述符
任何时候对文件的输入/输出都是通过文件描述符标识文件,而不是通过文件名(文件描述符类似于c标准库中的文件指针或MS-DOS中的文件句柄)
系统负责维护已打开文件的所有信息,用户程序只能通过文件描述符引用文件
大多数的输入/输出是通过键盘和显示器来实现的,因此为方便起见,UNIX对此做出了特别的安排
当命令解释程序(即“shell”)运行一个程序时,它将打开三个文件,对应的文件描述符分别是0,1,2,依次表示标准输入、标准输出和标准错误
如果程序从0中读,对1和2进行写,就可以进行输入/输出而不必关心打开文件的问题

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

在这种情况下,shell将文件描述符0(标准输入)和1(标准输出)的默认赋值改变为指定的文件
通常情况下,文件描述符2(标准错误)仍然与显示器相关联,这样出错信息仍然输出到显示器上
与管道相关的输入/输出同样有类似的特性
在任何情况下,文件赋值的改变都不是由程序完成的,而是由shell完成的
只要程序使用文件0作为输入,文件1和2作为输出,它无需知道程序的输入从哪里来,输出到哪里去

低级I/O——read和write

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

函数原型:
int read(int fd,char* buf,int n);
int write(int fd,char* buf,int n);

两个函数中,第一个参数是文件描述符,第二个参数是程序中用于存放要读或写数据的数组,第三个参数是要传输的字节数
两个函数的调用正常情况均返回实际传输的字节数,可以用整型变量接收,函数的返回值可能会小于请求的字节数
此外还存在一些特殊情况:
读文件时,如果返回值为0,表示已到达文件的末尾,如果返回值为-1,则表示发生了某种错误
写文件时,返回值是实际写入的字节数,如果返回值与请求写入的字节数不相等,此时说明写入文件过程中发生了错误
在一次调用中,读出或写入数据的字节数可以为任意大小
最常用值为1,即每次读出或写入1个字符(无缓冲),或者类似于1024或4096这样的与外围设备物理块大小

例:将输入复制到输出
#include"syscalls.h"
int copy(){
    char bufs[BUFSIZ];
    int n;
    while((n=(int)read(0,buf,BUFSIZE))>0)write(1,buf,n);
    return 0;
}

(K&R书中中使用syscalls.h头文件存放系统调用相关的函数原型和宏常量,不过该头文件不是标准的)
(UNIX C标准的存放系统调用相关内容的头文件为unistd.h)
(此外常量BUFSIZ存放在stdio.h,定义了该系统下缓冲区的最佳大小,通常值为1024,根据不同操作系统而言不一样)
上例中如果文件大小不是BUFSIZ的整数倍,
则对read的某次调用(最后一次)会返回一个较小的字节数,而write再按这个字节数写,最后再调用read返回0

例:使用read和write,实现getchar()
int getchar(){
    static char buf[BUFSIZ];
    static char*bufp;
    static int n=0;
    if(n==0){
        n=read(0,buf,sizeof buf);
        bufp=buf;
    }
    return (--n>=0)?*bufp++:EOF;
}

如果要在包含头文件<stdio.h>的情况下编译自定义的getchar(),有必要用#undef预处理指令取消名字getchar的宏定义
(宏替换发生在预处理阶段,先于函数调用)

除默认的标准输入、标准输出和标准错误文件外,其他文件都必须在读或写之前显式地打开
系统调用open和creat用于实现打开文件的功能,同样在c中可以通过函数open和creat访问这两个系统调用

函数原型:int open(char*name,int flags,int perms);

与fopen类似,参数name是一个包含文件名的字符串,第二个参数flags是一个int类型的值,说明以何种方式打开文件,主要的几个取值如下:

  • O_RDONLY 以只读方式打开文件
  • O_WRONLY 以只写方式打开文件
  • O_RDWR 以读写方式打开文件
  • O_CREAT 打开并创建文件,如果文件不存在就按照参数perms中给出的模式创建文件;

在System V UNIX系统中,flags常量定义在<fcntl.h>头文件,而在Berkeley(BSD)版本中则在<sys/file.h>中定义的
(同unistd.h,fcntl.h是UNIX C下存放关于文件管理方面的宏和函数原型的头文件)
第三个参数perms指定创建文件时的访问权限,只有当flags=O_CREAT时需要此参数,其余情况都可以省略或置0即可
系统调用open与c的fopen()很相似,不同的是后者返回一个文件指针,而前者返回一个文件描述符(int类型),如果发生错误open将返回-1
如果用open打开一个不存在的文件将导致错误,可使用creat系统调用创建新文件或覆盖旧文件

函数原型:int creat(char*name,int perms);

如果creat成功地创建了文件,它将返回一个文件描述符,否则返回-1
如果此文件已存在,creat将把该文件的长度截断为0,从而丢弃原先已有的内容,使用creat创建一个已存在的文件不会导致错误
如果要创建的文件不存在,则creat用perms指定的权限创建文件
在UNIX系统中,每个文件对应一个9比特的权限信息,每个位分别控制文件的所有者、所有者组和其他成员对文件的读、写和执行访问
因而通过一个三位的八进制数可以方便地说明不同的权限,如:

0755说明文件的所有者可以对它进行读、写和执行操作,而所有者组和其他用户只能进行读和执行操作
例:简单实现UNIX程序cp
#include<cstdarg>
void error(const char*fmt,...){
    va_list args;
    va_start(fmt,args);
    fprintf(stderr,"error:");
    vfprintf(stderr,fmt,args);
    fprintf(stderr,"\n");
    va_end(args);
    exit(1);
}
int cpmain(int argc,char*argv[]){
    int f1,f2,n;
    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=(int)read(f1,buf,BUFSIZ))>0)if(write(f2,buf,n)!=n)error("cp:write error on file %s",argv[2]);
    return 0;
}
这个版本仅仅能将文件复制到另一个文件,不允许用目录作为第二个参数,并目标文件的权限没有复制原文件而是重新指定的

其中标准库函数vprintf()与printf()函数类似,不同的是它用一个va_list参数取代了变长参数表,且此参数通过调用va_start进行初始化
同样,vfprintf()和vfsprintf()分别与fprintf()和fsprintf()类似
一个程序同时打开的文件数是有限制的(通常为20),相应地,如果一个程序需要同时处理许多文件,那么其必须重用文件描述符
函数close(in fd)用来断开文件描述符和已打开文件之间的连接,并释放此文件描述符,以供其他文件使用
close()与标准库中的fclose()函数相对应,但它不需要清洗(flush)缓冲区
如果程序通过exit()函数退出或从主程序中返回,所有打开的文件将被关闭
函数unlink(char*name)将文件name从文件系统中删除,它对应于标准库函数remove

随机访问——lseek

输入/输出通常是顺序进行的:每次调用read和write进行读写的位置紧跟在前一次操作的位置之后
但是有时候需要以任意的顺序访问文件,而系统调用lseek可以在文件中任意移动位置而不实际读写任何数据

函数原型:long lseek(int fd,long offset,int origin);

lseek将文件描述符为fd的文件的当前位置设置为offset,其中offset是相对于origin指定的位置而言的
origin的值可以为0、1或2,分别用于指定offset从文件开始、从当前位置或从文件结束处开始算起

如:为向一个文件尾部添加内容(在UNIX shell程序中使用重定向符>>或在库函数fopen中使用参数"a"),
则在写操作之前必须使用lseek(fd,0L,2);找到文件末尾(其中参数0L也可写成(long)0,或仅仅写成0,但必须与声明格式保持一致)

同理,若要返回文件开始处(即反绕),使用lseek(fd,0L,0);
使用lseek系统调用时,可以将文件视为一个大数组,其代价是访问速度会慢一些
lseek返回一个long类型的值,此值表示文件的新位置,若发生错误则返回-1
标准库函数fseek()与lseek类似,不同在于前者第一个参数是FILE*,且在错误发生时返回一个非0值

例:从文件任意位置读入任意数目字节,返回读入的字节数,发生错误返回1
int get(int fd,long pos,char*buf,int n){
    if(Iseek(fd,pos,0)>=0)return read(fd,buf,n);
    else return -1;
}

目录列表

除获取或修改文件具体内容外外,有时还需要获得文件系统的其他相关信息,目录列表程序就是其中的一个例子

如:UNIX命令ls,可以打印目录中的文件名及其他可选信息如文件长度、访问权限等,MS-DOS操作系统中dir命令也有类似功能

由于在UNIX中目录是一种文件,因此ls只需读目录文件即可获得所有的文件名
但如果需要获取文件的其他信息,如长度等,就需要使用其他系统调用
在其他一些系统(比如MS-DOS)中,甚至获取文件名也需要使用系统调用
在UNIX系统中,目录就是文件,它包含了一个文件名列表和一些指示文件位置的信息
“位置”是一个指向其他表(其i结点表)的索引,文件的i结点是存放除文件名外所有文件信息的地方
目录项通常仅包含两个条目:文件名和i结点编号
结构Dirent包含i结点信息和文件名,文件名最大长度由NAME_MAX设定,NAME_MAX值由系统决定
opendir返回一个指向称为DIR的结构的指针,该结构与FILE类似,可以被readdir和closedir使用,如:

typedef struct Dirent{          //可移植的目录项结构
    long ino{};                 //i结点编号
    char name[NAME_MAX+1]{};    //文件名加字符串结束符
}Dirent;
typedef struct DIR{             //最小的DIR:无缓冲等特性
    int fd{};                   //目录的文件描述符
    Dir ent d;                  //目录项
}DIR;
DIR*opendir(char*dirname);
Dirent*readdir(DIR*dfd);
void closedir(DIR*dfd);

关于目录项的信息存放在头文件<dirent.h>
系统调用stat以文件名作为参数,返回文件i结点中的所有信息,若出错则返回-1,如:

char*name;
struct stat stbuf;
int stat(char*,struct stat*);
stat(name,&stbuf);
stat用文件name的i结点信息填充结构stbuf,头文件<sys/stat.h>定义了stat的返回值的结构

该结构的一个典型形式为:

struct stat{            //由stat返回的i结点信息
    dev_t st_dev;       //i结点设备
    ino_t st_ino;       //i结点编号
    short st_mode;      //模式位
    short st_nlink;     //文件的总链接数
    short st_uid;       //所有者的用户id
    short st_gid;       //所有者的组id
    dev_t st_rdev;      //用于特殊的文件
    off_t st_size;      //用字符数表示的文件长度
    time_t st_atime;    //上一次访问的时间
    time_t st_mtime;    //上一次修改的时间
    time_t st_ctime;    //上一次改变i结点的时间
};
其中dev_t和ino_t等所有系统相关的类型在<sys/types.h>中定义
st_mode项包含了描述文件的一系列标志,在<sys/stat.h>定义,如:
#define S_IFMT 016000   //文件的类型
#define S_IFDIR 004000  //目录
#define S_IFCHR 002000  //特殊字符
#define S_IFBLK 006000  //特殊块
#define S_IFREG 010000  //普通