您的位置:首页 > 运维架构 > Linux

Linux&C语言文件学习笔记(三):文件I/O与系统API

2017-01-18 23:51 746 查看

一、文件描述符、文件表:

1、文件描述符:

操作系统中喜欢用整数来代表一系列内容,比如:内存地址是整数的十六进制形式、errno错误类型标志(0表示SUCCESS,无errno)、进程标识符(pid)(0表示内核交互进程)、线程标识符(tid)等都是非负整数。那么我们所说的文件描述符也是由一系列非负整数表示,其中0、1、2这三个数在每一个进程被创建时就已经被占用(0表示标准输入设备文件、1表示标准输出设备文件、2表示标准错误文件):

vim /usr/include/unistd.h   查看0、1、2的文件描述符与宏定义




用户所能自行分配的就从3开始(注意:每个进程都独自占有一套进程标识符,就像每个进程都拥有独立的4G内存页一样)。这种将标准输入输出以及标准错误与0、1、2分别关联的形式是一种惯例,但与内核无关,尽管如此,如果不遵循这种惯例,很多系统应用程序将无法运行,所以我们就当它是一种标准以遵循它。

一个4G内存的计算机其内存地址上限是0XFFFFFFFF,那么一个操作系统的文件描述符最大(上限)是多少呢?我们可以用“ulimit -n”来查看:



对于特殊需求而需要修改文件描述符上限的问题,可参考: 修改Linux系统下的最大文件描述符限制 这篇博客,我这里就不再过多涉及了。

2、进程表项、文件表项、V节点表项数据结构:

(1)基本概念:

内核使用3种数据结构表示一个进程打开的文件(结构如下图),它们之间的关系决定了文件在共享方面,一个进程对另一个进程可能产生的影响。



1)、每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表(即每个进程都拥有一套独立的文件描述符)。一个文件描述符关联一个文件描述符标志,指向一个文件表项(如:fd0关联描述符0,指向标准输入的文件表项)。

2)、内核为所有打开的文件维持一张文件表(每个进程独占一个,即使多进程共享文件,每个进程也都有自己的独立的文件表项以区分不同的进程不同的操作)。该文件表包括三部分:①文件状态标志:读写权限、O_APPEND与O_TRUNC等操作权限、阻塞状态、同步异步状态等;②当前文件偏移量offset;③指向该文件v节点表项的指针。

3)、每个打开的文件(设备)都有一个v节点(v-node)结构。v节点包括文件类型、对此为文件进行各种操作的指针。以及i节点(i-node,索引节点)i节点类似于内存地址,它存放了文件在磁盘上的地址(扇区与磁道编号)。i节点信息在打开文件的时候从磁盘上读入内存,在操作后可随时修改,如当前节点长度在lseek重新定位后会随时修改(显示当前相对于文件头的偏移量)。将与文件系统无关的i节点部分称为v节点。 (该关系是UNIX中采用,而在Linux中采用的是与文件系统相关的i节点和与文件系统无关的i节点,并没有采用v节点)

我们可用ls -i选项查看i-node索引号:



(2)、多进程共享文件数据结构:

如图所示,两个进程共享一个文件,而每个进程都有自己独立的一套文件描述符(独立的进程表项);对文件的独立操作(独立的文件表项)(每个进程都有自己对该文件的不同操作权限,不同的偏移量);以及共享的文件信息(v节点表项):



二、open()、create()、close():

1、函数原型:

/*与C标准库函数进行比较学习*/
#include<fcntl.h>
int open( const char * pathname, int flags, ...);
/*"..."参数为mode_t mode,如果是新建(O_CREAT)则需要第三个参数,如果不是新建则不需要(0XXX表示权限(0表示八进制形式),如:0666),即使是设置了0666,系统也会屏蔽掉某些权限以保证安全*/
int create( const char * pathname, mode_t mode);/*以只写的方式打开新建的文件*/

#include<unistd.h>
int close(int fd);/*关闭整数fd描述符指向的那个文件。*/


2、参数与返回值:

参数:

pathname:为打开或要创建的带路径文件名

flag:为设定打开的方式(列举常用flag):

①选择打开的权限,必须且只能包含其中一个:

O_RDONLY只读、O_WRONLY只写、O_RDWR读写;

②打开与操作方式:

O_CREAT不存在则创建,存在则打开;

O_EXCL不存在则新建,存在则返回-1表示错误(与O_CREAT结合使用);

O_TRUNC覆盖式写入,即打开文件后原有内容先清空再写入(与O_CREAT结合使用);

O_APPEND追加的方式打开,即直接在原有内容后继续写入.

其它参数均不太常用,故不作为重点。

fd:文件描述符

返回值:

出错均返回-1.

open:成功返回文件表中未使用的最小文件描述符;

create:成功返回为只写打开文件描述符;

close:成功返回0;

3、其它要点:

(1)、文件open的过程 :

先打开一个文件–>用文件表记录该文件信息–>在文件描述符总表(进程表项)中,找到没有使用的文件描述符(默认最小)–>把最小的文件描述符和文件表对应起来,放入文件描述符总表中。

(2)、create等效于:open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);

在早期open函数不能打开不存在的文件时,先用create函数创建一个新的只写文件并打开,然后用close关闭,再用open打开这个已经存在的文件才可以进行读操作。而后来open提供了O_CREAT选项以后,create函数基本不再使用。因为以上步骤可以用open(pathname, O_RDWR | O_CREAT | O_TRUNC, mode); 实现。

(3)、一个进程打开的文件,如果在关闭之前,该进程已经终止,那么该文件会被内核自动关闭,很多程序都使用了这一点,而不显示地调用close关闭。

三、write()、read():

1、函数原型、描述与返回值:

#include<unistd.h>

ssize_t read(int fd,void * buf,size_t count);
/*fd为open打开的文件返回的描述符,从buf中读取,返回读到的个数,void*表示不止能读字符串,任何指针均可。count为读取的个数*/
ssize_t write(int fd,const void * buf,size_t count);
/*写到buf中,与read合用时count(写入的字节数)一般设定为read的返回的个数,返回写入的数据字节数。*/


read()、write()失败时均返回-1,并且设置errno错误信息。无论是读或者是写,每读/写一个字符,文件读写位置就会自动向后移动一个字节,read()函数当到达文件尾时返回0。


2、注意与测试:

read()、write()都是不带缓冲的I/O(unbuffered I/O),而fread()、fwrite()都是自带缓冲区的。所以说缓冲区需要自行设定,缓冲区的大小决定了系统函数能否充分发挥其高性能的优点,测试: C语言文件操作标准库函数与Linux系统函数效率比较

四、lseek()函数:

1、函数原型:

#include<sys/types.h>
#include<unistd.h>
off_t lseek(int fd,off_t offset ,int whence);/*lseek()函数是System Call,是fseek()函数的底层实现,功能特点与fseek也基本相同*/


2、参数与返回值:

参数:

fd:文件描述符;

offset:是偏移量(正负);新打开的文件offset为0,该函数只将偏移量记录在内核中并不会引起任何I/O操作;当offset大于文件长度时,不会出错,对该文件的下一次读写将会加长该文件,会形成文件空洞。空洞不占用磁盘空间,空洞之后写的内容则会占用。

whence:是偏移量的起始位置:

SEEK_SET:文件开头,偏移量不能为负(开发中更多使用)

SEEK_CUR:当前位置

SEEK_END:文件末尾,偏移量不能为正

返回值:

当调用成功时则返回目前的读写位置,也就是距离文件开头多少个字节。若有错误则返回-1,并设置相关errno错误信息。

注意:Linux系统不允许lseek()对tty作用,此项动作会令lseek()返回ESPIPE。

tty:/dev/tty:标准输入输出(键盘、显示器)。

eg:(cat < /dev/tty输入重定向到tty—键盘)、(echo hello > /dev/tty输出重定向到tty—显示器,直接在显示器上显示hello)。

三种常用方式:

1) 欲将读写位置移到文件开头时:lseek(fd, 0, SEEK_SET);

2) 欲将读写位置移到文件尾时:lseek(fd, 0, SEEK_END);

3) 想要取得目前文件位置时:lseek(fd, 0, SEEK_CUR);

3、简单测试:

/*test.txt内容为26个英文字母*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>

/*errno错误处理*/
void error_print(const char * ptr){
perror(ptr);
exit(EXIT_FAILURE);
}
int main (void)
{
int fd = open("test.txt",O_RDONLY);/*以只读方式打开*/
if(fd == -1)    error_print("open");

char buf[30] = {0};
int res = read(fd,buf,sizeof(buf));/*先将test.txt读一遍*/
if(res == -1)    error_print("read");
printf("%s",buf);/*读出来的内容输出*/

memset(buf, 0, sizeof(buf));/*缓冲区清空后再读*/
lseek(fd,0,SEEK_SET);/*读写位置重新定位到文件开头*/
res = read(fd,buf,3);/*从头读三个写入缓冲区*/

lseek(fd,10,SEEK_CUR);/*从文件指针当前位置后移10个字节*/
res = read(fd,buf+3,2);/*在从文件指针所在位置读2个写入缓冲区,buf+3防止覆盖*/

printf("%s\n",buf);/*将读到的5个字符输出*/
close(fd);
return 0;
}


结果如下:



五、dup()、dup2():

1、函数原型、参数与返回值:

#include<unistd.h>
int dup(int oldfd);
/*复制oldfd文件描述符,即为oldfd作一份拷贝,并返回复制后得到的新描述符(系统选择的未使用的最小描述符)*/
int dup2(int oldfd,int newfd);
/*将oldfd复制到newfd,即newfd是oldfd的拷贝,返回newfd(用户自己选择的描述符)*/


返回值:成功返回新的文件描述符(dup返回复制的文件描述符,dup2返回newfd),失败返回-1,并设置合适的errno值。

2、函数解析:

dup()和dup2()都可以复制文件描述符,所谓复制拷贝指的是:复制文件描述符的指向,而不是复制一个独立于被复制对象的新的文件表。

区别

dup()返回的是系统自行查找的未使用的最小值;

dup2()返回的是第二个参数,如果该值已经被使用,会先强制关闭然后再使用。

共同点:

新的文件描述符newfd(或者dup的返回的描述符)与参数oldfd指的是同一个文件,共享所有的锁定、读写位置和各项权限。

使用文件描述符时,内存中对应一个文件表,在文件表中,会记录关于内存中文件表的信息和硬盘上的文件的信息,其中,i节点是文件在硬盘上的地址。dup()和dup2()复制文件描述符,但不复制对应的文件表。

3、测试与图解:

/*dup.txt*/
dup:73321081111181013212111111744321091213210010197114321211173210111433

/*dup2.txt*/
dup2:733997109321211111171143210010197114321079711010311411711110610511033

/*dup.c*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>

void error_print(const char * ptr){
perror(ptr);
exit(EXIT_FAILURE);
}
int main (void)
{
int fd3 = open("dup.txt",O_RDONLY);
if(fd3 == -1)   error_print("open dup.txt");
int fd4 = dup(fd3);

int fd5 = open("dup2.txt",O_RDONLY);
if(fd5 == -1)   error_print("open dup2.txt");
int fd6 = dup2(fd3,fd5);

printf("%d,%d,%d,%d\n",fd3,fd4,fd5,fd6);

char buf[30] = {0};
read(fd3,buf,sizeof(buf));
printf("fd3:%s\n",buf);

memset(buf,0,strlen(buf)); //   lseek(fd4,0,SEEK_SET);
read(fd4,buf,sizeof(buf));
printf("fd4:%s\n",buf);

memset(buf,0,strlen(buf));//    lseek(fd5,0,SEEK_SET);
read(fd5,buf,sizeof(buf));
printf("fd5:%s\n",buf);

memset(buf,0,strlen(buf));//    lseek(fd6,0,SEEK_SET);
read(fd6,buf,sizeof(buf));
printf("fd6:%s\n",buf);

close(fd3);//close(fd4);与close(fd5);与clsoe(fd6);都是一样的效果
return 0;
}


cat dup*.txt结果:



注销掉三个lseek()的运行结果:



取消三个lseek()的注释运行结果:



由上图测试我们可以知道,fd3~fd6操作的都是dup.txt,在fd3读完dup.txt之后,文件指针位于文件末尾,所以其他三个都没有读到任何信息。对于fd3和fd4,似乎没有疑惑,但是fd5打开的是dup2.txt而且fd6接收的是fd5,所以fd5与fd6不是应该打印dup2.txt吗?其实不然,画张图来说明一下:



在int fd6 = dup2(fd3,fd5);执行的时候,dup2的第二个参数与进程表项中的已有描述符冲突,所以dup2要先强行关闭fd5先前打开的dup2.txt,然后再拷贝fd3指向的dup.txt文件的地址给fd5,并返回给fd6,所以fd5与fd6都指向dup.txt。(红色虚线表示关闭,绿色虚线表示重新指向)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  内核 文件 操作系统