unix系统是c语言编写的,Unix系统编程
C语言系列:8、Unix系统接口-文件操作文章目录C语言系列:8、Unix系统接口-文件操作1、文件描述符2、低级IO-读/写3、打开、创建、关闭unlink 4。随机存取-lseek 5。fopen和getc函数的实例实现。实例目录列表7。实例存储分配器8。最后
UNIX操作系统通过一系列的系统调用来提供服务,这些系统调用实际上是操作系统中的函数,可以被用户程序调用。本章将介绍如何在C语言程序中使用一些重要的系统调用。如果读者正在使用UNIX,这一章将对你有直接的帮助。这是因为我们经常需要借助系统调用来获得最高的效率,或者访问一些标准库中没有的函数。然而,即使读者在其他操作系统上使用C语言,本章中的示例也将帮助您更深入地理解C语言编程。不同系统的代码都差不多,只是一些细节不同。因为ANSI C标准函数库是基于UNIX系统的,所以学习本章中的过程也有助于更好地理解标准库。
本章包括三个主要部分:输入/输出、文件系统和存储分配。其中,前两部分的内容要求读者对UNIX系统的外部特征有一定的了解。
Linux是类Unix的,所以本章的内容也适用于Linux。我们在上一节中学习的输入和输出是基于标准库的,因此它们在所有操作系统中都是通用的。但是,基于系统调用的输入和输出仅适用于该系统。目前我们大部分的后端开发都是基于*nix的,所以基本上我们会学习基于Unix的系统调用的应用开发。为了维护自己的生态,所有的操作系统一般都会做一些改变(比如明明都可以用c/c,但是微软要c#,苹果要swift,一些系统化的东西更方便,兼容自己的语言调用),以防止失去自己系统生态的竞争力,给别人做嫁衣,而开源生态更注重便捷性和开放性。毕竟这样更有利于普及。
1.文件描述符* *在UNIX操作系统中,所有外围设备(包括键盘和显示器)都被视为文件系统中的文件。因此,所有的输入/输出都应该通过读取或写入文件来完成。* *也就是说,外围设备和程序之间的所有通信都可以通过一个接口来处理。
通常,在读取或写入文件之前,您必须首先通知系统这一意图。这个过程称为打开文件。如果您正在编写一个文件,您可能需要首先创建该文件,或者您可能需要放弃文件中预先存在的内容。系统检查您的权限(文件是否存在?你能拿到它吗?),如果一切正常,操作系统会返回一个小的非负整数给程序,这个整数叫做文件描述符。每当我输入/输出文件时,文件都是由文件描述符而不是文件名来标识的。(文件描述符类似于标准库中的文件指针或MS-DOS中的文件句柄。)系统负责维护所有打开文件的信息。用户程序只能通过文件描述符引用文件,因为大部分输入/输出是通过键盘和显示器实现的。为了方便起见,UNIX为此做了特殊的安排。命令解释器(即“shell”)运行一个程序时,会打开三个文件,对应的文件描述符是0、1、2,依次代表标准输入、标准输出、标准错误。如果程序从文件0读取并写入1和2,它可以输入/输出,而不必担心打开文件。
程序的用户可以通过和重定向程序的I/O:
Prog输入文件名输出文件名在这种情况下,shell将文件描述符0和1的默认赋值更改为指定的文件。通常,文件描述符2仍然与显示器相关联,以便将错误消息输出到显示器。与管道相关的I/O也有类似的特征。在任何情况下,文件赋值的改变都不是由程序完成的,而是由shell完成的。只要程序使用文件0作为输入,文件1和2作为输出,它就不会知道程序的输入来自哪里,输出去哪里。
2.低级IO-读/写输入和输出是通过读写系统调用实现的。在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,即每次读取或写入一个字符(无缓冲),或者是类似1024 ~ 4096的值,对应外围设备的物理块大小。用更大的值调用这个函数可以达到更高的效率,因为系统调用的次数减少了。
结合上面的讨论,我们可以写一个简单的程序把输入复制到输出,功能上和第一章的复制程序是一样的。该程序可以将任何输入复制到任何输出,因为I/O可以重定向到任何文件或设备。
#include syscalls.h
main() /*将输入复制到输出*/
{
char buf[BUFSIZ];
int n;
而((n=read(0,buf,BUFSIZ)) 0)
write(1,buf,n);
返回0;
}我们将系统调用的函数原型集中在一个头文件syscalls.h中,因此
该程序将包含头文件。但是,文档的名称并不标准。
syscalls.h头文件中也定义了参数BUFSIZ。对于所使用的操作系统,该值
是一个更合适的值。如果文件大小不是BUFSIZ的倍数,调用read将返回较少的字节数,然后根据这个字节数进行写入。之后,调用read将返回0。
为了更好的掌握相关概念,下面介绍如何使用read和write构造类似getchar的东西,
putchar等高级功能。例如,下面是getchar函数的一个版本,它通过从标准输入中一次读入一个字符来实现无缓冲输入。
#include syscalls.h
/* getchar:无缓冲的单字符输入*/
int getchar(void)
{
char c;
return (read(0,c,1)==1)?(无符号字符)c:EOF;
}其中,C必须是char类型的变量,因为read函数需要一个字符指针类型的参数(C)。在return语句中将C转换为无符号char类型可以消除符号扩展问题。
getchar的第二个版本一次读入一组字符,但一次只输出一个字符。
#include syscalls.h
/* getchar:简单缓冲版本*/
int getchar(void)
{
静态char buf[BUFSIZ];
静态char * bufp=buf
静态int n=0;
if (n==0) { /*缓冲区为空*/
n=read(0,buf,sizeof buf);
bufp=buf
}
return ( - n=0)?(无符号字符)* bufp:EOF;
}如果要用头文件stdio.h编译这些版本的getchar函数,就需要使用
#undef预处理指令取消了名称getchar的宏定义,因为在头文件中,getchar是作为宏实现的。
3.打开、创建、关闭和取消链接除了默认的标准输入、标准输出和标准错误文件,其他文件必须在读取或写入前显式打开。系统调用open和creat来实现这个功能。
类似于open第七章讨论的fopen,区别在于前者返回一个文件描述符,它只是一个int类型的数值。而后者返回一个文件指针。如果出现错误,open将返回-1。
#包含fcntl.h
int fd
int open(char *name,int flags,int perms);
fd=open(名称、标志、烫发);像fopen一样,参数名是一个包含文件名的字符串。第二个参数flags是一个int类型的值,它解释了如何打开文件。主要值如下:
O _以只读方式打开文件
O_WRONLY以只写方式打开文件。
O_RDWR以读写模式打开文件。在System V UNIX系统中,这些常量在头文件fcntl.h中定义,而在Berkeley(BSD)版本中,它们在sys/file.h中定义
您可以使用以下语句打开文件进行读取:
fd=open(name,O_RDONLY,0);在本章的讨论中,参数perms of open的值始终为0。
如果用open打开一个不存在的文件,将会导致错误。您可以使用creat系统调用来创建新文件或覆盖现有的旧文件,如下所示:
int creat(char *name,int perms);
fd=creat(名字,perms);如果creat成功创建了文件,它将返回一个文件描述符,否则它将返回-1。如果该文件已经存在,creat会将该文件的长度截断为0,从而丢弃原始内容。使用creat创建现有文件不会导致错误。
如果要创建的文件不存在,creat将使用参数perms指定的权限创建文件。在UNIX中
在系统中,每个文件对应一个9位权限信息,该信息控制文件的所有者、所有者组和其他成员对文件的读、写和执行访问。因此,不同的权限很容易用一个3位数的八进制数来解释。例如,0755表示文件的所有者可以读取、写入和执行该文件,而所有者组和其他成员只能读取和执行该文件。
creat的用法通过一个简化的UNIX程序cp来说明。这个程序将一个文件复制到另一个文件。我们写的版本只能复制一个文件,不允许把目录作为第二个参数。此外,目标文件的权限是重新定义的,而不是复制的。
#包含stdio.h
#包含fcntl.h
#include syscalls.h
#为所有者、组和其他人定义PERMS 0666/* RW */
void错误(char *,);
/* cp:将f1复制到f2 */
main(int argc,char *argv[])
{
int f1,f2,n;
char buf[BUFSIZ];
如果(argc!=3)
错误(“用法:cp从到”);
if ((f1=open(argv[1],O_RDONLY,0))==-1)
错误( cp:无法打开%s ,argv[1]);
if ((f2=creat(argv[2],PERMS))==-1)
错误( cp:无法创建%s,模式o,
argv[2],PERMS);
而((n=read(f1,buf,BUFSIZ)) 0)
if(写(f2,buf,n)!=n)
错误( cp:文件%s上的写入错误,argv[2]);
返回0;
}此程序创建的输出文件的固定权限为0666。使用将在8.6节中讨论的stat系统调用,您可以获得一个现有文件的模式,并将这个模式分配给它的副本。
注意,函数error类似于函数printf,可以用变长参数表调用。接下来,通过错误信
number的实现展示了如何使用printf函数族的另一个成员vprintf。标准库函数vprintf函数
类似于number printf函数,不同的是它用一个参数代替了变长参数表,这个参数是通过调用va_start宏初始化的。同样,vfprintf和vsprintf函数分别类似于fprintf和sprintf函数。
#包含stdio.h
#包含stdarg.h
/* error:打印错误消息并退出*/
void错误(char *fmt,)
{
va_list参数;
va_start(args,fmt);
fprintf(stderr, error:);
vprintf(stderr,fmt,args);
fprintf(stderr, \ n );
va _ end(args);
出口(1);
}一个程序同时打开的文件数量是有限制的(通常是20个)。因此,如果一个程序需要同时处理许多文件,它必须重用文件描述符。函数close(int fd)用于将文件描述符从打开的文件中断开,并为其他文件释放文件描述符。close函数对应于标准库中的fclose函数,但是它不需要刷新缓冲区。如果程序通过exit函数退出或从主程序返回,所有打开的文件都将被关闭。
unlink(char *name)函数从文件系统中删除文件名,这与标准库函数相对应。
移除.
4.随机存取- lseek输入/输出通常是按顺序完成的:每次读写调用的位置紧跟在前一次操作的位置之后。但是,有时需要以任意顺序访问文件,系统调用lseek可以移动到文件中的任意位置,而无需实际读取或写入任何数据:
long lseek(int fd,long offset,int origin);将文件描述符为fd的文件的当前位置设置为offset,其中offset相对于orgin指定的位置。后续的读写操作将从这个位置开始,origin的值可以是0、1或2,分别用于指定从文件、当前位置或文件结尾开始偏移。例如,为了将内容添加到文件的末尾(使用UNIX shell程序中的重定向器或系统调用fopen中的参数“a”),必须在写入之前使用以下系统调用来找到文件的末尾:
lseek(fd,0L,2);要返回到文件的开头(即rewind),可以使用下面的调用:
lseek(fd,0L,0);注意,参数0L也可以写成(long)0,或者只是0,但是系统调用lseek的声明必须一致。
使用lseek系统调用时,可以将文件视为一个大数组,代价是访问速度变慢。例如,下面的函数将从文件中的任意位置读取任意数量的字节,并且它将返回读取的字节数,如果发生错误,则返回-1。
#include syscalls.h
/*get:从位置pos读取n个字节*/
int get(int fd,long pos,char *buf,int n)
{
if (lseek(fd,pos,0)=0) /* get to pos */
返回read(fd,buf,n);
其他
return-1;
}lseek系统调用返回一个long值,该值指示文件的新位置,如果出现错误,则返回-1。标准库函数fseek类似于系统调用lseek,除了前者的第一个参数是FILE *类型,并在出错时返回非零值。
5.示例fopen和getc函数的实现。这里以标准库函数fopen和getc的一种实现方法为例,说明如何组合这些系统调用。
让我们回忆一下,标准库中的文件不是由文件描述符描述的,而是由文件指针描述的。文件指针是指向包含文件各种信息的结构的指针,包含以下内容:指向缓冲区的指针,通过缓冲区可以一次读入文件的一大块内容;记录缓冲器中剩余字符数的计数器;指向缓冲区中下一个字符的指针;文件描述符;描述读/写模式的标志;描述错误状态的标志等。
描述文件的数据结构包含在头文件stdio中。任何需要使用标准输入/输出库中的函数的程序都必须在源文件中包含这个头文件(通过#include指令包含头文件)。该文件也包含在库中的其他函数中。在以下典型的stdio.h代码片段中,只有标准库中的其他函数使用的名称以下划线开头,因此它们通常不会与用户程序中的名称冲突。的所有标准库函数都遵循这一约定。
#定义NULL 0
#定义EOF (-1)
#定义BUFSIZ 1024
# define OPEN _ MAX 20/* MAX #一次打开的文件数*/
typedef struct _iobuf {
int cnt/*左侧字符*/
char * ptr/*下一个字符位置*/
char * base/*缓冲器的位置*/
int标志;/*文件访问模式*/
int fd/*文件描述符*/
}文件;
extern FILE _ iob[OPEN _ MAX];
#定义标准输入(_iob[0])
#定义标准输出(_iob[1])
#define stderr ( _iob[2])
enum _flags {
_READ=01,/*文件打开以供读取*/
_WRITE=02,/*文件打开进行写入*/
_UNBUF=04,/*文件未被缓冲*/
_EOF=010,/*此文件上发生了EOF */
_ERR=020 /*此文件出现错误*/
};
int _ fill buf(FILE *);
int _flushbuf(int,FILE *);
#定义feof(p) ((p)- flag _EOF)!=0)
#定义ferror(p) ((p)- flag _ERR)!=0)
#define fileno(p) ((p)- fd)
#定义getc(p) ( - (p)- cnt=0\
?(无符号字符)*(p)- ptr : _fillbuf(p))
#定义putc(x,p) ( - (p)- cnt=0\
?*(p)- ptr=(x) : _flushbuf((x),p))
#define getchar() getc(标准输入)
# define putcher (x) putc ((x),stdout)宏getc一般将计数器减1,将指针移动到下一个位置,然后返回字符。(如前所述,一个长的#define语句可以用反斜杠分成几行。但是,如果计数值变成负值,getc调用function _fillbuf来填充缓冲区,重新初始化结构的内容,并返回一个字符。返回的字符属于无符号类型。确保所有字符都是正面的。
虽然这里不想讨论一些细节,但程序中给出了putc函数的定义,以说明其操作与getc函数非常相似。当缓冲区已满时,它将调用函数_flushbuf。此外,我们还包括访问错误输出、文件结束状态和文件描述符的宏。
让我们开始编写函数fopen。fopen函数的主要作用是打开文件,定位到合适的位置,设置标志位表示相应的状态。它不分配任何缓冲空间。第一次读取文件时,缓冲区的分配由函数fillbuf完成。
#包含fcntl.h
#include syscalls.h
#为所有者、组和其他人定义PERMS 0666/* RW */
文件*打开(字符*名称,字符*模式)
{
int fd
FILE * fp
if(*模式!= r *模式!= w *模式!=a )
返回NULL
for(FP=_ iob;fp _ iob OPEN _ MAXfp)
if ((fp- flag (_READ _WRITE))==0)
打破;/*找到空闲插槽*/
if (fp=_iob OPEN_MAX) /*没有空闲插槽*/
返回NULL
if (*mode==w )
fd=creat(名字,PERMS);
else if (*mode==a) {
if ((fd=open(name,O_WRONLY,0))==-1)
fd=creat(名字,PERMS);
lseek(fd,0L,2);
}否则
fd=open(name,O_RDONLY,0);
if (fd==-1) /*无法访问名称*/
返回NULL
FP-FD=FD;
FP-CNT=0;
FP-base=NULL;
fp- flag=(*mode==r )?_ READ:_ WRITE;
返回FP;
}这个版本的fopen函数并没有涉及到标准C的所有访问模式,然而,加入这些模式并不需要太多代码。特别是这个版本的fopen不能识别表示二进制访问模式的B标志,因为这个模式在UNIX系统中是没有意义的。同时,它不能识别允许同时读写的标志。
对于一个特定的文件,第一次调用getc函数,计数值为0,所以function _fillbuf必须调用一次。如果_fillbuf发现文件不是以读模式打开的,它会立即返回EOF;否则,它将尝试分配一个缓冲区(如果读操作是以缓冲的方式完成的)。
缓冲区建立后,_fillbuf调用read填充缓冲区,设置计数值和指针,返回缓冲区中的第一个字符。后续的_fillbuf调用将发现缓冲区已经被分配。
#include syscalls.h
/* _fillbuf:分配并填充输入缓冲区*/
int _fillbuf(FILE *fp)
{
int bufsize
if ((fp- flag (_READ_EOF_ERR))!=_READ)
返回EOF
bufsize=(fp- flag _UNBUF)?1:BUFSIZ;
if (fp- base==NULL) /*还没有缓冲区*/
if((FP-base=(char *)malloc(bufsize))==NULL)
返回EOF/*无法获取缓冲区*/
FP-ptr=FP-base;
fp- cnt=read(fp- fd,fp- ptr,bufsize);
if ( - fp- cnt 0) {
if (fp- cnt==-1)
FP-flag =_ EOF;
其他
FP-flag =_ ERR;
FP-CNT=0;
返回EOF
}
return(无符号字符)* FP-ptr;
}最后就是如何执行这些函数。我们必须在array _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。
6.示例目录列表我们经常需要在文件系统上执行另一个操作来获取关于文件的信息,而不是读取文件的具体内容。一个例子是目录程序,比如UNIX命令ls,它打印目录中的文件名和其他可选信息,比如文件长度、访问权限等等。MS-DOS操作系统中的dir命令也有类似的功能。
因为UNIX中的目录是一种文件,ls只需要读取这个文件就可以得到所有的文件名。但是,如果你需要得到文件的其他信息,比如长度,你需要使用系统调用。在其他系统中,甚至获取文件名也需要系统调用,例如在MS-DOS系统中。无论实现是否与特定系统相关,我们都需要提供一种独立于系统的方式来访问文件信息。
这将由下面的程序fsize来解释。Fsize程序是ls命令的一种特殊形式,它打印命令。
行参数表中指定的所有文件的长度。如果其中一个文件是一个目录,fsize程序将递归地调用这个目录。如果命令行中没有参数,fsize程序将处理当前目录。
我们先来回顾一下UNIX文件系统的结构。在UNIX系统中,目录是一个文件,它包含一个文件名列表和一些指示文件位置的信息。“位置”是对其他表(即I节点表)的索引。文件的I节点是存储除文件名之外的所有文件信息的地方。一个目录通常只包含两个条目:文件名和inode号。
不幸的是,在系统的不同版本中,目录的格式和确切内容是不同的。因此,为了将非便携部分分开,我们将任务分成两部分。外层定义了一个名为Dirent的结构和三个函数opendir、readdir和closedir,它们提供了对目录条目中的名称和I节点号的独立于系统的访问。我们将使用这个接口编写fsize程序,然后解释如何在与Version 7和System V UNIX系统具有相同目录结构的系统上实现这些功能。其他情况留作练习。
结构目录包含I节点号和文件名。文件名的最大长度由NAMZ_MAX设置,NAME_MAX的值由系统决定。OpenDIR返回一个名为DIR的结构的指针,类似于结构文件,它将被readdir和closedir使用。所有这些信息存储在头文件dirent.h中
#define NAME_MAX 14/*最长文件名组成部分;*/
/*系统相关*/
typedef结构{ /*可移植目录条目*/
龙伊诺;/*信息节点号*/
char NAME[NAME _ MAX 1];/* name \0 终止符*/
} Dirent
typedef struct { /* minimal DIR:无缓冲,等等。*/
int fd/*目录的文件描述符*/
方向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(名称,ST buf);结构stbuf填充了其文件名的I节点信息。头文件sys/stat.h包含stat的描述。
返回值的结构。这种结构的典型形式如下:
stat */返回的struct stat /* inode信息
{
dev _ t st _ dev/*信息节点的设备*/
ino _ t st _ ino/*信息节点号*/
短st _ mode/*模式位*/
短st _ nlink/*指向文件的链接数*/
短st _ uid/*所有者用户id */
短st _ gid/*所有者群组id */
dev _ t st _ rdev/*对于特殊文件*/
off _ t st _ size/*以字符为单位的文件大小*/
time _ t st _ atime/*上次访问的时间*/
time _ t st _ mtime/*上次修改时间*/
time _ t st _ ctime/*最初创建时间*/
};这个结构中的大部分值已经在评论中解释过了。dev_t和ino_t等类型在头文件中。
按照sys/types.h中的定义,这个文件必须包含在程序中。
st_mode项包含一系列描述文件的标志。这些标志仅在sys/stat.h We中定义
需要处理文件类型的相关部分:
#定义S_IFMT 0160000/*文件类型:*/
#define S_IFDIR 0040000/*目录*/
#define S_IFCHR 0020000/*特殊字符*/
# define S _ if blk 0060000/* block special */
# define S _ IFREG 0010000/* regular */
/* .*/我们开始写程序fsize吧。如果stat调用获得的模式表明文件不是目标
记录,很容易得到文件的长度,直接输出。但是,如果文件是一个目录,则必须逐个处理目录中的文件。因为这个目录可能包含子目录,所以这个过程是递归的。
主程序main处理命令行参数,并将每个参数传递给函数fsize。
#包含stdio.h
#包含字符串. h
#include syscalls.h
#include fcntl.h /*读写标志*/
# include sys/types . h/* typedef s */
# include stat返回的sys/stat.h /*结构*/
#包含“dirent.h”
void fsize(char *)
/*打印文件名*/
main(int argc,char **argv)
{
if (argc==1) /*默认值:当前目录*/
fsize( . );
其他
while ( - argc 0)
fsize(* argv);
返回0;
}函数fsize打印文件的长度。但是,如果这个文件是一个目录,fsize首先调用dirwalk。
函数处理它包含的所有文件。注意如何使用sys/stat.h文件中的标志名S_IFMT和S_IFDIR来判断该文件是否为目录。括号是必要的,因为运算符的优先级低于==运算符。
int stat(char *,struct stat *);
void dirwalk(char *,void(* fcn)(char *));
/* fsize:打印文件名 name */
void fsize(char *name)
{
struct stat stbuf
if (stat(name,ST buf)=-1){
fprintf(stderr, fsize:无法访问%s\n ,name);
返回;
}
if((ST buf . ST _ mode S _ IFMT)==S _ IFDIR)
dirwalk(名称,fsize);
printf(%8ld %s\n ,stbuf.st_size,name);
}函数dirwalk是一个通用函数,对目录中的每个文件调用一次函数fcn。塔首
首先打开目录,遍历其中的每个文件,对每个文件调用这个函数,然后关闭目录返回。因为fsize函数为每个目录调用dirwalk函数,所以这两个函数递归地相互调用。
#定义最大路径1024
/* dirwalk:将fcn应用于目录中的所有文件*/
void dirwalk(char *dir,void (*fcn)(char *))
{
char name[MAX _ PATH];
Dirent * dp
DIR * dfd
if ((dfd=opendir(dir))==NULL) {
fprintf(stderr, dirwalk:无法打开%s\n ,dir);
返回;
}
while ((dp=readdir(dfd))!=NULL) {
if (strcmp(dp- name, . ))==0
strcmp(dp- name,.))
继续;/*跳过自身和父项*/
if(strlen(dir)strlen(DP-name)2 sizeof(name))
fprintf(stderr, dirwalk:名称%s %s太长\n ,
dir,DP-name);
否则{
sprintf(name, %s/%s ,dir,DP-name);
(*fcn)(名称);
}
}
closedir(DFD);
}每次调用readdir都会返回一个指向下一个文件信息的指针。如果目录中没有要处理的文件,该函数将返回NULL。每个目录都包含自己的项目。以及父目录“…”,处理时必须跳过,否则会导致无限循环。
至此,代码与目录的格式无关。下一步是在特定系统上提供opendir、readdir和closedir的最简单版本。以下函数适用于Version 7和System V UNIX系统,它们使用头文件(sys/dir.h)中的目录信息,如下所示:
#ifndef DIRSIZ
#定义目录14
#endif
struct direct { /*目录条目*/
ino _ t d _ ino/*信息节点号*/
char d _ name[DIRSIZ];/*长名称没有“\ 0”*/
};某些版本的系统支持更长的文件名和更复杂的目录结构。
Type ino_t是typedef定义的类型,用来描述I-node表的索引。在我们通常的使用中
在系统中,这种类型是无符号的短整型,但是这种信息不应该在程序中使用。因为不同系统中的类型可能不同,所以最好使用typedef定义。的所有“系统”类型都可以在文件sys/types.h)中找到。
Opendir函数首先打开目录,验证这个文件是一个目录(调用系统调用fstat,与stat相关
类似,但它以文件描述符作为参数),然后分配一个目录结构并保存信息:
int fstat(int fd,struct stat *);
/* opendir:为readdir调用打开一个目录*/
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)
返回NULL
DP-FD=FD;
返回DP;
}closedir函数用于关闭目录文件并释放内存空间:
/* closedir:关闭opendir打开的目录*/
void closedir(DIR *dp)
{
if (dp) {
关闭(DP-FD);
免费(DP);
}
}最后,函数readdir使用read系统调用来读取每个目录条目。如果目录位置当前不可用
使用(因为删除了一个文件),其I节点号为0,跳过该位置。否则,将I节点号和目录名放在一个静态结构中,并向用户返回一个指向该结构的指针。每次调用readdir函数都会覆盖前一次调用获得的信息。
#include sys/dir.h /*本地目录结构*/
/* readdir:按顺序读取目录条目*/
目录*读取目录(目录*dp)
{
struct direct dirbuf/*本地目录结构*/
静态方向d;/* return:可移植结构*/
while (read(dp- fd,(char *) dirbuf,sizeof(dirbuf))
==sizeof(dirbuf)) {
if (dirbuf.d_ino==0) /*插槽未使用*/
继续;
d . ino=dirbuf . d _ ino;
strncpy(d.name,dirbuf.d_name,DIRSIZ);
d . name[DIRSIZ]= \ 0 ;/*确保终止*/
返回d;
}
返回NULL
}虽然fsize程序很特殊,但确实说明了一些重要的思想。首先,很多程序并不是“系统程序”,它们只是使用了操作系统维护的信息。对于这类程序,非常重要的一点是,信息的表示只出现在标准头文件中,使用它们的程序只需要在文件中包含这些头文件,不需要包含相应的声明。其次,可以为系统相关的对象创建一个独立于系统的接口。标准库中的函数就是一个很好的例子。
7.例子——存储分配器我们在第五章给出了一个面向堆栈的存储分配器,功能有限。本节对要写的版本没有限制,malloc和free可以任意顺序调用。Malloc在必要时调用操作系统来获得更多的存储空间。这些程序解释了以系统无关的方式编写系统相关代码时应该考虑的问题,也展示了结构、联合和typedef的实际应用。
Malloc不从编译时确定的固定大小的数组中分配存储空间,而是在需要时从操作系统请求空间。因为程序中有些地方可能不会调用malloc申请空间(即通过其他方式申请空间),malloc管理的空间不一定是连续的。这样,空闲存储空间以空闲块链表的形式组织起来,每个块包含一个长度,一个指向下一个块的指针和一个指向自己存储空间的指针。这些块按照存储地址的升序排列,最后一块(最高地址)指向第一块(见图8-1)。
当有应用程序请求时,malloc将扫描空闲块链表,直到找到足够大的块。该算法被称为“首次拟合”;与之相反的算法是“最佳拟合”,即寻找满足条件的最小块。如果块正好符合请求的大小,它将从链表中移除并返回给用户。如果块太大,就分成两部分:大小合适的块返回给用户,剩下的留在空闲块链表中。如果找不到足够大的块,就向操作系统申请一个大块,添加到空闲块链表中。
释放过程也是先搜索空闲块链表,找到合适的位置可以插入释放的块。如果与释放的块相邻的任一侧是空闲块,那么这两个块将合并成一个更大的块,这样存储空间就不会有太多的碎片。因为空闲块链表是按地址升序链接在一起的,所以很容易判断相邻块是否空闲。
在第5章中,我们提出了确保malloc函数返回的存储空间满足要保存的对象的对齐要求的问题。虽然有不同类型的机器,但每个特定的机器都有最受限制的类型:如果最受限制的类型可以存储在特定的地址中,那么所有其他类型也可以存储在该地址中。在一些机器中,最受限制的类型是双类型;在其他机器中,最受限制的类型是int或long。
一个空闲块包含一个指向链表中下一个块的指针,一个块大小的记录和一个指向空闲空间本身的指针。块开始处的控制信息称为“报头”。为了简化块的对齐,所有块的大小必须是磁头大小的整数倍,并且磁头已经正确对齐。这是通过一个联合实现的,该联合包含所需的标头结构和具有最严格对齐要求的类型的实例。在下面的程序中,我们假设long类型是最具限制性的类型:
typedef长对齐;/*与长边界对齐*/
联合头{ /*块头*/
结构{
联合标题* ptr/*如果在空闲列表中,则下一个块*/
无符号大小;/*该块的大小*/
} s;
对齐x;/*强制块对齐*/
};
typedef联合头标头;在这个联合中,将永远不会使用Align字段。它仅用于在最坏的情况下强制每个报头满足对齐要求。
在malloc函数中,请求的长度(以字符为单位)将被舍入,以确保它是头大小的整数倍。实际分配的块将为报头本身包含一个以上的单元。实际分配的块大小将被记录在报头的大小字段中。malloc函数返回的指引将指向空闲空间,而不是块的头部。用户可以对获得的存储空间做任何事情,但是如果在分配的存储空间之外写入数据,块链表可能会被破坏。图8-2显示了malloc返回的块。
其中的size字段是必需的,因为由malloc函数控制的块不一定是连续的,所以它不是
可以通过指针算术运算来计算它的大小。
变量base表示空闲块链表的头。第一次调用malloc函数时,freep为空,系统
将创建一个退化的自由块链表,该链表只包含一个大小为0的块,该块指向自身。在任何情况下,当请求空闲空间时,将搜索空闲块链表。搜索从最后找到空闲块(freep)的地方开始。这种策略可以保证链表是统一的。如果找到的块太大,就把它的尾部返回给用户,这样初始块的头只需要修改size字段。无论如何,返回给用户的指针指向块中的空闲存储空间,即比指向头的指针大一个单位。
静态标题基础;/*空列表开始*/
静态Header * freep=NULL/*空闲列表的开始*/
/* malloc:通用存储分配器*/
void *malloc(无符号nbytes)
{
Header *p,* prevp
Header *moreroce(无符号);
无符号nunits
nunits=(nbytes sizeof(Header)-1)/sizeof(Header)1;
if ((prevp=freep)==NULL) { /*还没有空闲列表*/
base . s . ptr=free ptr=prev ptr=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)/* exact */
prevp-s . ptr=p-s . ptr;
否则{ /*分配尾端*/
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)
返回NULL/*一个都不剩*/
}
morecore函数用于向操作系统请求存储空间,其实现细节因系统而异。因为从系统请求存储空间是一个开销很大的操作,所以我们不想每次调用malloc函数时都执行这个操作。基于这种考虑,morecore函数请求至少NALLOC个单元。这个较大的块将根据需要被分成较小的块。开始
完size字段之后,morecore函数调用free函数把多余的存储空间插入到空闲区域中。
UNIX 系统调用sbrk(n)返回一个指针,该指针指向n 个字节的存储空间。如果没有空闲空间,尽管返回NULL可能更好一些,但sbrk调用返回-1。必须将-1 强制转换为char *类型,以便与返回值进行比较。而且,强制类型转换使得该函数不会受不同机器中指针表示的不同的影响。但是,这里仍然假定,由sbrk调用返回的指向不同块的多个指针之间可以进行有意义的比较。ANSI标准并没有保证这一点,它只允许指向同一个数组的指针间的比较。因此,只有在一般指针间的比较操作有意义的机器上,该版本的malloc函数才能够移植。
#define NALLOC 1024/* minimum #units to request */
/* morecore: ask system for more memory */
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) /* no space at all */
return NULL;
up = (Header *) cp;
up- s.size = nu;
free((void *)(up+1));
return freep;
}我们最后来看一下free 函数。它从freep 指向的地址开始,逐个扫描空闲块链表,寻找可以插入空闲块的地方。该位置可能在两个空闲块之间,也可能在链表的末尾。在任何一种情况下,如果被释放的块与另一空闲块相邻,则将这两个块合并起来。合并两个块的操作很简单,只需要设置指针指向正确的位置,并设置正确的块大小就可以了。
/* free: put block ap in free list */
void free(void *ap)
{
Header *bp, *p;
bp = (Header *)ap - 1; /* point to block header */
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; /* freed block at start or end of arena */
if (bp + bp- size == p- s.ptr) { /* join to upper nbr */
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) { /* join to lower nbr */
p- s.size += bp- s.size;
p- s.ptr = bp- s.ptr;
} else
p- s.ptr = bp;
freep = p;
}虽然存储分配从本质上是与机器相关的,但是,以上的代码说明了如何控制与具体机器相关的部分,并将这部分程序控制到最少量。typedef和union的使用解决了地址的对齐(假定sbrk返回的是合适的指针)问题。类型的强制转换使得指针的转换是显式进行的,这样做甚至可以处理设计不够好的系统接口问题。虽然这里所讲的内容只涉及到存储分配,但是,这种通用方法也适用于其它情况。
8. 最后C的基础语法基本上就是这些,再往深可以看c primer plus(可以当成字典来翻,太厚了),C三剑客(推荐),接下来C语言相关内容我会继续总结:数据结构和算法-C语言描述,再然后进行stm32系列的总结,arduino、51和PLC虽然大学学了,但是目前工作上基本没有用过,有机会接触可能会学习总结,没有机会的话可能C部分暂时就告一段落了(面试题目前由于没有面试的需求,暂时也不会总结,至于Linux驱动和内核部分的学习和总结我暂时是按照Linux相关内容归纳的,不单独放到C系列中)。
©
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。