C语言学习——UNIX系统接口
UNiX系统
UNIX操作系统通过一系列的系统调用提供服务,这些系统调用实际上是操作系统内的函数,它们可以被用户程序调用。
文件描述符
在UNIX操作系统中,所有的外围设备(包括键盘和显示器)都被看作是文件系统中的文件,因此,所有的输入/输出都要通过读文件或写文件完成。
即通过一个单一的接口就可以处理外围设备和程序直接的所有通信。
Unix上用文件描述符,指代文件。
文件描述符是整型
0/1/2分别代表标准输入,输出,错误
程序的使用者可通过<和>重定向程序的I/O:
prog <输入文件名>输出文件名
低级I/O —— read 和 write
输入与输出是通过read和write系统调用实现的。
这两个函数中,第一个参数是文件描述符,第二个参数是程序中存放读或写的数据的字符数组,第三个参数是要传输的字节数。
int n_read = read(int fd, char *buf, int n);
int n_written = written(int fd, char *buf, int n);
每个调用返回实际传输的字节数。
getchar函数的一个版本,它通过每次从标准输入读入一个字符来实现无缓冲输入。
#include "syscalls.h"
/* getchar函数: 无缓冲的单字符输入 */
int getchar(void)
{
char c;
return (read(0, &c, 1) == 1 ) ? (unsigned char) c : EOF;
}
getchar的第二个版本一次读入一组字符,但每次只输出一个字符。
#include "syscalls.h"
/* getchar函数: 简单的带缓冲区的版本 */
int getchar(void)
{
static char buf[BUF SIZ];
static char *bufp = buf;
static int n = 0;
if (n == 0)
{
/* 缓冲区为空 */
n = read(0, buf, sizeof buf);
bufp = buf;
}
return (--n >= 0) ? (unsigned char) *bufp++ : EOF;
}
open、creat、close和unlink
除了默认的标准输入、标准输出和标准错误文件外,其他文件都必须在读或写之前显示地打开。系统调用open和creat用于实现该功能。
open与fopen的区别:
前者返回一个文件描述符,它仅仅只是一个int类型的数字,而后者返回一个文件指针。如果发生错误,open将返回-1。
#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打开一个不存在的文件,则将导致错误。可以使用creat系统调用创建新文件或覆盖已有的旧文件,如下所示:
int creat(char *name, int perms);
fd = creat(name, perms);
如果成功创建了文件,它将返回一个文件描述符,否则返回-1。
如果此文件已存在,creat将把该文件的长度截断为0,从而丢弃原先已有的内容。使用creat创建一个已存在的文件不会导致错误。
如果要创建的文件不存在,则creat用参数perms指定的权限创建文件。
标准库函数 vprintf 函数与printf 函数类似,所不同的是,它用一个参数取代了变长参数表,且此参数通过调用va_start宏进行初始化。
同样,vprintf 和 vsprintf 函数分别与 fprintf 和 sprintf 函数类似。
#include <stdio.h>
#include <stdarg.h>
/* error函数: 打印一个出错信息,然后终止 */
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);
}
函数 unlink(char*name) 将文件 name 从文件系统中删除,它对应于标准库函数 remove 。
随机访问——lseek
输入/输出通常是顺序进行的:每次调用read和write进行读写的位置紧跟在前一次操作的位置之后。
调用lseek可以在文件中任意移动位置而不实际读写任何数据:
long lseek(int fd, long offset, int origin);
下面的函数将从文件的任意位置读入任意数目的字节,它返回读入的字节数,若发生错误,则返回-1。
long lseek(int fd, long offset, int origin);
#include "syscalls.h"
/*get函数: 从pos位置处读入n个字节 */
int get(int fd, long pos, char *buf, int n)
{
if (lseek(fd, pos, 0) >= 0) /* 移动到位置 pos处 */
return read(fd, buf, n);
else
return -1;
}
标准库函数fseek与系统调用了lseek类似,所不同的是,前者的第一个参数是 FILE *类型,且在发生错误时返回一个非0值。
实例——fopen和getc函数的实现
#define NULL 0
#define EOF (-1)
#define BUFSIZ 1024
#define OPEN_MAX 20 /* 一次最多可打开的文件数 */
typedef struct _iobuf
{
int cnt; /* 剩余的字符数 */
char *ptr; /* 下一个字符的位置 */
char *base; /* 缓冲区的位置 */
int flag; /* 文件访问模式 */
int fd; /* 文件描述符 */
} FILE;
extern FILE _iob[OPEN_MAX];
#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])
enum _flags
{
_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)
#define getc(p) (--(p)->cnt >= 0 \
? (unsigned char) *(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 函数的主要功能是打开文件,定位到合适的位置,设置标志位以提示相应的状态。
它不分配任何缓冲区空间,缓冲区的分配是在第一次读文件时由函数 _fillbuf 完成的。
#include <fcntl.h>
#include "syscalls.h"
#define PERMS 0666 /* R所有者、所有者组合其他成员都可以读写 */
/* fopen函数:打开文件,并返回文件指针 */
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; /* 寻找一个空闲位 */
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函数时计数值为0,这样就必须调用一次函数 _fillbuf。
如果 _fillbuf 发现文件不是以读写方式打开的,它将立即返回EOF;否则,它将试图分配一个缓冲区(如果读操作是以缓冲方式进行的话)。
建立缓冲区后, _fillbuf 调用read填充此缓冲区,设置计数值和指针,并返回缓冲区中的第一个字符。随后进行的 _fillbuf 调用会发现缓冲区以及分配。
#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 (unsigned char) *fp->ptr++;
}
最后一件事情便是如何执行这些函数。我们必须定义和初始化数组 _iob 中的 stdin、stdout和 stderr值:
FILE _iob[OPEN_MAX] =
{
/* stdin, stdout, stderr */
{ 0, (char *) 0, (char *) 0, _READ, 0 },
{ 0, (char *) 0, (char *) 0, _WRITE, 1 },
{ 0, (char *) 0, (char *) 0, _WRITE | _UNBUF, 2 }
};
该结构中 flag 部分的初值表明,将对 stdin 执行读操作、对 stdout 执行写操作、对 stderr 执行缓冲方式的写操作。
实例——目录列表
目录是一个文件,包含文件名列表,和文件位置。
文件位置用索引号指示。
目录项通常仅仅包含:the filename and an inode number.
文件i节点存放除文件名外文件信息
为了分离出不可移植的部分,我们把任务分成两部分。
外层定义了一个称为Dirent的结构和3个函数opendir、readdir、closedir,它们提供与系统无关的对目录项中的名字和 i 结点编号的访问。
结构Dirent包含 i 结点编号和文件名。文件名的最大长度由 NAME_MAX设定, NAME_MAX的值由系统决定。
opendir返回一个指向称为DIR的结构的指针,该结构与结构FILE类似,它将被readdir和closedir使用。
所有这些信息存放在头文件dirent.h中。
// dirent.h.
#define NAME_MAX 14 /* 最长文件名:由具体的系统决定 */
/* system-dependent */
typedef struct
{
/* 可移植的目录项*/
long ino; /* i结点编号 */
char name[NAME_MAX+1]; /* 文件名加结束符 '\0' */
}
Dirent;
typedef struct
{
/* 最小的DIR:无缓冲等特性 */
int fd; /* 目录的文件描述符 */
Dirent d; /* 目录项 */
}
DIR;
DIR *opendir(char *dirname);
Dirent *readdir(DIR *dfd);
void closedir(DIR *dfd);
系统调用stat以文件名作为参数,返回文件的 i 结点中的所有信息;若出错,则返回-1。
如下所示:
char *name;
struct stat stbuf;
int stat(char *, struct stat *);
stat(name, &stbuf);
它用文件name的i结点信息填充结构stbuf。头文件<sys/stat.h>中包含了描述stat的返回值得结构。
该结构的一个典型形式如下所示:
<sys/stat.h>
<sys/types.h>
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结点的时间 */
};
st_mode项包含了描述文件的一系列标志,这些标志在<sys/stat.h>中定义。我们只需要处理文件类型的有关部分:
#define S_IFMT 0160000 /* 文件的类型 */
#define S_IFDIR 0040000 /* 目录 */
#define S_IFCHR 0020000 /* 特殊字符 */
#define S_IFBLK 0060000 /* 特殊块 */
#define S_IFREG 0010000 /* 普通 */
/* ... */
使用了头文件<sys/dir.h>中的目录信息,如下所示:
<sys/dir.h>
#ifndef DIRSIZ
#define DIRSIZ 14
#endif
struct direct
{
/* 目录项 */
ino_t d_ino; /* i结点编号 */
char d_name[DIRSIZ]; /* 长文件名不包含'\0' */
};
实例——存储分配程序
malloc在必要时调用操作系统以获取更多的存储空间。
malloc并不是从一个在编译时就确定的固定大小的数组中分配存储空间,而是在需要时向操作系统申请空间。因为程序中的某些地方可能不通过malloc调用申请空间(也就是说,通过其他方式申请空间),所以,malloc管理的空间不一定是连续的。
这样,空闲存储空间以空闲块链表的方式组织,每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。
这些块按照存储地址的升序组织,最后一块(最高地址)指向第一块。
malloc函数返回的指针将指向空闲空间,而不是块的头部。
用户可对获得的存储空间进行任何操作,但是,如果在分配的存储空间之外写入数据,则可能会破坏块链表。
由malloc返回的块。
学习参考资料:
《C程序设计语言》第2版 新版
https://blog.csdn.net/x13262608581/article/details/108724872