《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
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值