1.写在前面
前面我们已经介绍完了C语言的一些常用知识,以及标准的输入输出,这篇博客我们我们来介绍UNIX系统接口的调用。UNIX操作系统通过一系列的系统调用提供服务,这些系统调用用实际上是操作系统内的函数,它们可以被用户程序调用。本篇博客主要介绍C语言程序中使用一些重要的系统调用。
2.文件描述符
Linux中一切皆文件,所以我们需要了解一下文件描述符。
在UNIX操作系统中,所有的外围设备都被看作文件系统中的文件,因此,所有的输入/输出都要通过读文件或写文件完成。
通过情况下,在读或写文件之前,必须先将这个意图通知系统,该过程称为打开文件。
如果是写一个文件,则可能需要先创建该文件,也可能需要丢弃该文件中原先已存在的内容。系统检查你的权利,如果一切正常,操作系统将向程序返回一个小的非负整数,该整数称为文件描述符。
任何时候对文件的输入/输出都是通过文件描述符标识文件,而不是通过文件名标识文件。
因为大多数的输入/输出是通过键盘和显示器来完成的,为了方便起见,UNIX对此做了特别的安排。当命令解释程序运行一个程序的时候,它将打开3个文件,对应的文件描述符分别为0,1,2依次表示标准输入、标准输出、标准错误。如果程序从文件0中读,对1和2进行写,就可以进行输入/输出而不必关心打开文件的问题。
3.低级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,则表示发生了某种错误。
在写文件时,返回值是实际写入的字节字节数。如果返回值与请求写入的字节数不相等,则说明发生了错误。
我们来看下如下的程序,具体的代码如下:
#include "syscalls.h"
main()
{
char buf[BUFSIZ];
int n;
while ((n = read(0, buf, BUFSIZ)) > 0)
write(1, buf, n);
return 0;
}
4.open、create、close和unlink
除了默认的标准输入、标准输出和标准错误文件外,其它文件都必须在读或写之前显示打开。
open和前面的fopen相似,不同的是,前者返回一个文件描述符,它仅仅知识一个int类型的数值。而后者返回一个文件指针。如果发生错误,open将返回-1。
#include <fcntl.h>
int fd;
int open(char *name, int flags, int perms);
fd = open(name, flags, perms);
参数name是一个包含文件名的字符串。第二个参数flags是一个int类型的值,它说明以何种方式打开文件,主要的几个值如下所示:
O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWR 以读写方式打开文件
如果用open打开一个不存在的文件,则将导致错误。可以使用creat系统调用创建新文件或覆盖已有的旧文件,如下所示:
int creat(char *name, int perms);
fd = creat(name, perms);
如果create成功创建了文件,它将返回一个文件描述符,否则返回-1。如果文件已存在,creat将该文件的长度截断为0,从而丢弃原先已有的内容。使用creat创建一个已经存在的文件不会导致错误。
如果要创建的文件不存在,则creat用参数perms指定的权限创建文件。我们可以看如下的程序,具体的如下:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define PERMS 0666
void error(char *, ...);
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;
}
该程序创建的输出文件具有固定的权限0666。
注意,函数error类似于函数printf,在调用时可带变长参数表。
一个程序同时打开的文件数是有限制的(通常为20)。相应的,如果一个程序需要同时处理许多文件,那么它必须重用文件描述符。函数close(int fd) 用来断开文件描述符和已打开文件之间的连接,并释放此文件描述符,以供其他文件使用。close函数与标准库中的fclose函数相对应,但它不需要清洗(flush)缓冲区。如果程序通过exit函数退出或从主程序中返回,所有打开的文件将被关闭。
函数unlink(char * name)将文件nmae从文件系统中删除,它对应于标准库的函数remove。
5.随机访问–lseek
输入/输出通常是顺序进行的:每次调用read和write进行读写的位置紧跟在前一次操作的位置之后。但是,有时候需要以任意顺序访问文件,系统调用lseek可以在文件中任意移动位置而不实际读写任何数据。
long lseek(int fd, long offset, int origin);
将文件描述符为fd的文件的当前位置设置为offset,其中offset是相对于orgin指定的位置而言的。随后进行的读写操作将此位置开始,origin的值可以为0、1或2,分别用于指定offset从文件开始、从当前位置或从文件结束处开始算起。
使用lseek系统调用时,可以将文件视为一个大数组,其代价是访问速度会慢一些。例如,下面的函数将从文件的任意位置读入任意数目的字节,它返回读入的字节数,若发生错误,则返回-1。
#include <unistd.h>
int get(int fd, long pos, char *buf, int n) {
if (lseek(fd, pos, 0) >= 0)
return read(fd, buf, n);
else
return -1;
}
lseek系统调用返回一个long类型的值,此值表示文件的新位置,若发生错误,则返回-1。
6.实例–fopen和getc函数的实现
标准库中文件不是通过文件描述符描述的,而是使用文件指针描述的。
文件指正是一个执行包含文件各种信息的结构的指针,该结构包含下列内容:
一个指向缓冲区的指针,通过它可以一次读入文件的一大块内容;一个记录缓冲区中剩余的字符数的计数器;一个指向缓冲区中下一个字符的指针;文件描述符;描述读/写模式的标志;描述错误的标志等。
下面我们来看fopen函数的主要功能就是打开文件,具体的代码如下:
typedef struct _iobuf {
int cnt;
char *ptr;
char *base;
int flag;
int fd;
} FILE;
extern FILE _iob[OPEN_MAX];
#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; 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;
}
7.实例–存储分配程序
malloc并不是从一个编译时就确定的固定大小的数组中分配存储空间,而是从需要时向操作系统申请空间。malloc管理的空间不一定是连续的。这样,空闲存储空间以空闲链表的方式组织,每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。这些块按照存储地址的升序组织,最后一块指向第一块
当有申请请求时,malloc将扫描空闲块链表,直到找到一个足够大的块为止。
如果该块恰好与请求的大小相符合,则将它从链表中移走并返回给用户。如果该块太大,则将它分成两部分:大小合适的块返回给用户,剩下的部分留在空闲块链表中。如果找不到一个足够大的块,则向操作系统申请一个大块并加入到空闲块链表中。
释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合成一个更大的块,这样存储空间不会有太多的碎片。因为空闲块链表是以地址的递增顺序链接在一起的,所以很容易判断相邻的块是否空闲。
空闲块包含一个指向链表中下一块的指针、一个块大小的记录和一个指向空闲空间的本身指针。位于块开始处的控制信息称为头部。为了简化块的对齐,所有块的大小必须是头部大小的整数倍。且头部已正确地对齐。这是通过一个联合实现的,该联合包含所需的头部结构以及一个对齐要求最受限的类型的实例。
在malloc函数中,请求的长度将被舍入,以保证它是头部大小的整数倍。实际分配的块将多包含一个单元,用于头部本身。实际分配的块的大小将被记录在头部的size字段中。malloc函数返回的指引将指向空闲空间,而不是块的头部。用户可对获得的存储空间进行任何操作,但是,如果在分配的存储空间之外写入数据,则可能会破坏块链表。
其中size字段是必须的,因为由malloc函数控制的块不一定是连续的,这样就不可能通过指针算术运算计算其大小。
变量base表示空闲块链表的头部。第一次调用malloc函数时,freep为NULL,系统将创建一个退化的空闲块链表,它只包含一个大小为0的块,且该块指向它自己。任何情况下,当请求空闲空间时,都将搜索空闲块链表。搜索从上一次找到空闲块的地方开始。该策略可以保证链表是均匀的。如果找到的块太大,则将其尾部返回给用户,这样,初始块的头部只需要修改size字段即可。在任何情况下,返回给用户的指针都指向块内的空闲存储空间,即比指向头部的指针大一个单元。
static Header base;
static Header *freep = NULL;
void *malloc(unsigned nbytes) {
Header *p, *prevp;
Header *moreroce(unsigned);
unsigned nunits;
nunits = (nbytes + sizeof(Header) - 1) / sizeof(header) + 1;
if ((prevp = freep) == NULL) {
base.s.ptr = freeptr = prevptr = &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;
}
}
函数morecore用于向操作系统请求存储空间,其实现细节因系统的不同而不同。
最后我们来看下free函数。它从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->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->size == bp) {
p->s.size += bp->s.size;
p->s.ptr = bp->s.ptr;
} else
p->s.ptr = bp;
freep = p;
}
8.写在最后
至此整个C的入门就结束了,这后面还需要多加练习,不然还是不行,代码一定要多写。
|