Lunix I/O总结
文件概念:
定义:
文件:一组相关数据的有序集合。
文件名:这个数据集合的名称。
按类型分类:
常规文件
ASCII码文件 : ASCII 是可以用任何文字处理程序阅读的简单文本文件
二进制的文件: 图形文件及文字处理程序等计算机程序都属于二进制文件
由于很难严格区分文本文件和二进制文件的概念,所以我们可以简单地认为,如果一个文件专门用于存储文本字符的数据,没有包含字符以外的其他数据,我们就称之为文本文件,除此之外的文件就是二进制文件。
七种:bcd-lsp
b (block):块设备文件 (鼠标,U盘等等)伪文件,放在/dev目录中
c (character):字符设备文件(顺序的数据流设备,对这种设备的读写是按字符进行) 伪
d (directory):目录
- :普通文件(纯文本文件(ASCII),二进制文件(binary), 数据格式文件(data)各种压缩文件)
l(link) :链接文件 (软连接文件,硬链接文件格式与源文件相同)
s :socket 套接字文件 网络通信的基本操作单元
p (pipe):管道文件 管道是一种两个进程间进行单向通信的机制。
系统调用与库函数
系统调用
在linux中,将程序的运行空间分为内核空间与用户空间(内核态和用户态),在逻辑上它们之间是相互隔离的,因此用户程序不能访问内核数据,也无法使用内核函数。当用户进程必须访问内核或使用某个内核函数时,就得使用系统调用(System Call)。在Linux中,系统调用是用户空间访问内核空间的唯一途径。
系统调用就是一种特殊的接口。通过这个接口,用户可以访问内核空间。系统调用规定了用户进程进入内核的具体位置。
应用程序接口API(Application Programming Interface) ,是程序员在用户空间下可以直接使用的函数接口
库函数
库函数(Library function)是把函数放到库里,供别人使用的一种方式。方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。一般放在.lib文件中。
(1)库函数是语言或应用程序的一部分,而系统调用是内核提供给应用程序的接口,属于系统的一部分
(2)库函数在用户地址空间执行,系统调用是在内核地址空间执行,库函数运行时间属于用户时间,系统调用属于系统时间,库函数开销较小,系统调用开销较大
(3)库函数是有缓冲的,系统调用是无缓冲的
(4)系统调用依赖于平台,库函数并不依赖
标准IO与文件IO
标准io:就是库函数
标准I/O指的是ANSI C中定义的用于I/O操作的一系列函数。只要操作系统安装了C库,就可以调用标准I/O。换句话说,若程序使用标准I/O函数,那么源代码无需进行任何修改就可以在其他操作系统上编译,具有更好的可移植性。
除此之外,由于标准I/O封装了缓冲区,使得在读写文件的时候减少了系统调用的次数,提高了效率。在执行系统调用的时候,Linux必须从用户态切换到内核态,在内核中处理相应的请求,然后再返回用户态。如果频繁地执行系统调用则会增加这种开销。标准I/O为了减少这种开销,采取缓冲机制,为用户空间创建缓冲区,读写时优先操作缓冲区,在必须访问文件时(例如缓冲区满、强制刷新、文件读写结束等情况)再通过系统调用将缓冲区的数据读写实际文件中,从而避免了系统调用的次数。
通过库函数的方式对文件进行操作,数据被暂存到缓存区中,直到满足一定条件,才会实时写入
优点:减少系统调用,有利于保护硬件
缺点:数据不会实时写入,可能会丢失
文件io:就是系统调用;
不带缓存的IO
通过系统调用的方式对文件进行操作,数据会实时写入
优点:实时写入数据,不会丢失
缺点:频繁的系统调用,增加系统开销,不利于保护硬件
标准I/O提供了三种类型的缓存
行缓存
} 缓存区大小: 1024字节(1K)。
} 刷新缓存 :程序正常结束、缓存区满、 ’\n’ 、使用fflush函数
全缓存
} 缓存区大小:4096字节(4K)
} 刷新缓存 :程序正常结束、缓存区满、使用fflush函数
不带缓存
} 标准I/O库不对字符进行缓冲,例如标准出错stderr。
} 很多的人机交互界面要求不可全缓存。
} 标准出错决不会是全缓存的。
在任何时刻,可以使用fflush强制刷新一个数据流
1.全缓冲:这种情况下,当缓冲区被填满后才进行实际的I/O操作。对于存放在磁盘上的普通文件用标准I/O打开时默认是全缓冲的。当缓冲区满或者执行刷新缓冲区(fflush)操作才会进行磁盘操作。
2.行缓冲:这种情况下,当在输入/输出中遇到换行符时执行I/O操作。–>标准输入/输出流(stdin/stdout)就是使用行缓冲。
3.无缓冲:不使用缓冲区进行I/O操作,即对流的读写操作会立即操作实际文件。标准出错流(stderr)是不带缓冲的,这就使得当发生错误时,错误信息能第一时间显示在终端上,而不管错误信息是否包含换行符。
fflush()
头文件 #include <stdio.h>
函数原型 int fflush(FILE *stream);
功能 可强制刷新一个流
参数 stream:指定的流指针
返回值 成功:0 失败:EOF
流和FILE对象
文件指针
FILE指针:每个被使用的文件都在内存中开辟一个区域,用来存放文件的有关信息,这些信息是保存在一个结构体类型的变量中,该结构体类型是由系统定义的,取名为FILE。
标准I/O库的所有操作都是围绕流(stream)来进行的,在标准I/O中,流用FILE *来描述。
FILE *
FILE *就是定义一个结构体指针变量,用于标识和操作一个文件,称之为流指针或者流
流(stream)
定义:所有的I/O操作仅是简单的从程序移进或者移出,这种字节流,就称为流。
分类:文本流/二进制流。
文本流:文本流是由字符文件组成的序列,每一行包含0个或多个字符并以’\n’结尾。在流处理过程中所有数据以字符形式出现,’\n’被当做回车符CR和换行符LF两个字符处理,即’\n’ASCII码存储形式是0x0D和0x0A。当输出时,0x0D和0x0A转换成’\n’
二进制流:二进制流是未经处理的字节组成的序列,在流处理过程中把数据当做二进制序列,若流中有字符则把字符当做ASCII码的二进制数表示。’\n’不进行变换。
标准I/O预定义3个流,当进程创建或者开启的时候,系统就会先分配三个流指针
标准输入 | 0 | 从终端输入数据 | stdin |
---|---|---|---|
标准输出 | 1 | 向终端输出数据 | stdout |
标准错误输出 | 2 | 将错误信息输出到终端 | stderr |
打开流 fopen( )
头文件 | #include <stdio.h> |
---|---|
函数原型 | FILE *fopen(const char *path, const char *mode); |
功能 | 打开或者创建一个文件,返回一个流指针 |
参数 | path:文件名,可以添加路径,如果不添加路径,默认就为当前路径 |
mode:对当前指定文件的操作权限 | |
返回值 | 成功:流指针 失败:NULL |
fopen() - mode参数
r | 以只读的方式打开一个文件,定位到文件的起始位置(文件必须存在) |
---|---|
r+ | 以读写的方式打开一个文件,定位到文件的起始位置(文件必须存在) |
w | 以只写的方式代开一个文件,如果文件不存在则创建,如果存在则清空, 定位到文件的起始位置。 |
w+ | 以读写的方式代开一个文件,如果文件不存在则创建,如果存在则清空, 定位到文件的起始位置 |
a | 以只写的方式代开一个文件,如果文件不存在则创建,如果存在则追加, 定位到文件的末尾位置 |
a+ | 以读写的方式代开一个文件,如果文件不存在则创建,如果存在则追加, 定位到文件的末尾位置 |
出错处理
全局错误码errno
在errno.h中定义,全局可见
错误值定义为“EXXX”形式,如EACCESS
处理规则
如果没有出错,则errno值不会被一个例程清除,即只有出错时,才需要检查errno值
头文件 | #include <stdio.h> |
---|---|
函数原型 | void perror(const char *s); |
功能 | 打开或当函数调用失败后输出相应的错误信息 |
参数 | s:错误信息的提示信息 |
返回值 | 成功:流指针 失败:NULL |
例如: FILE *fp = NULL;
if((fp = fopen(“file.txt”, “w”)) == NULL)
{
perror(“fail to fopen!”);
return -1;
}
如果文件file.txt不存在,程序执行时会打印如下信息:
Fail to fopen: NO such file or directory
头文件 | #include <string.h> #include <errno.h> |
---|---|
函数原型 | char *strerror(int errnum); |
功能 | 出错处理函数 |
参数 | errnum 错误号 |
返回值 | 错误原因的字符串 |
FILE *fp = NULL;
if((fp = fopen(“file.txt”, “r”)) == NULL)
{
printf(“fail to fopen %s\n”, strerror(errno));
printf("%d\n", errno);
return 1;
}
如果文件file.txt不存在,程序执行时会打印如下信息:
Fail to fopen: NO such file or directory
errno 可在/usr/include/errno.h中查询或者使用 strerror显示错误信息
fclose()
头文件 | #include <stdio.h> |
---|---|
函数原型 | int fclose(FILE *fp); |
功能 | 关闭一个流指针 |
参数 | fp:指定的流指针 |
返回值 | 成功:0 失败:EOF |
fprintf( )
头文件 | #include <stdio.h> |
---|---|
函数原型 | int fprintf(FILE *stream, const char *format, …); |
功能 | 向指定的文件里面写数据 |
参数 | stream:指定的流指针 |
返回值 | 成功:写入的字节数 失败:0 |
FILE *fp = NULL;
if((fp = fopen(“file.txt”, “w”)) == NULL)
{
perror(“fail to fopen!”);
return -1;
}
fprintf(fp, “111111111111111\n”);
char s[] = “2222222222222”;
fprintf(fp, “this is %s\n”, s);
此时file.txt中被写入
111111111111111
this is 2222222222222
fprintf(fp, “%d”, buffer); 是将格式化的数据写入文件
fprintf(文件指针,格式字符串,输出表列);
fwrite(&buffer, sizeof(int), 1, fp);是以二进位位方式写入文件
fwrite(数据,数据类型大小(字节数),写入数据的最大数量,文件指针);
由于fprintf写入时,对于整数来说,一位占一个字节,比如1,占1个字节;10,占2个字节;100,占3个字节,10000,占5个字节
所以文件的大小会随数据的大小而改变,对大数据空间占用很大。
而fwrite是按二进制写入,所以写入数据所占空间是根据数据类型来确定,比如int的大小为4个字节(一般32位下),那么整数10所占空间为4个字节,100、10000所占空间也是4个字节。所以二进制写入比格式化写入更省空间。
因此,
对于1 2 3 4 5 6 7 8 9 0 十个整数,用fprintf写入时,占10个字节;而用fwrite写入时,占40个字节。
对于100 101 102 103 104 105 106 107 108 109 110 这十个整数,用fprintf写入时,占30个字节;而用fwrite写入时,占40个字节。
对于10000 10100 10200 10300 10400 10500 10600 10700 10800 10900 11000 这十个整数,用fprintf写入时,占50个字节;而用fwrite写入时,还是占40个字节。
读写流
调用fopen()成功打开流之后,可在三种不同类型的非格式化I/O中进行选择,对其进行读、写操作:
每次一个字符的I/O:使用fgetc()/fputc()一次读或写一个字符,如果流是带缓存的,则标准I/O函数处理所有缓存。
每次一行的I/O:使用fgets()和fputs()一次读或写一行。每行都以一个新行符终止。当调用fgets()时,应说明能处理的最大行长。
直接I/O(二进制IO):fread()和fwrite()函数支持这种类型的I/O。每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中读或写一个结构。
每次一个字符的I/O
头文件 | #include <stdio.h> |
---|---|
函数原型 | int fgetc(FILE *stream); |
功能 | 从一个文件里面读取一个字节 |
参数 | stream:指定的流指针 |
返回值 | 成功:读取到的字节 失败:EOF 如果文件内容读取完毕,也返回EOF |
头文件 | #include <stdio.h> |
函数原型 | int fputc(int c, FILE *stream); |
功能 | 向一个文件写入一个字节 |
参数 | c:要写入的数据 stream:指定的流指针 |
返回值 | 成功:要写入的数据 失败:EOF |
while((c = fgetc(fp)) != EOF)
{
printf(“c = [%c] %d\n”, c, c);
}
打印出文件中数据
FILE *fp_r = NULL, *fp_w = NULL;
//打开源文件
if((fp_r = fopen(argv[1], “r”)) == NULL)
{perror(“fail to fopen!”); return -1;}
//创建或者打开目标文件
if((fp_w = fopen(argv[2], “w”)) == NULL)
{perror(“fail to fopen!”); return -1;}
//读取``源文件,写入到目标文件中
int c;
while((c = fgetc(fp_r)) != EOF)
{
fputc(c, fp_w);
}
实现复制操作
每次一行字符的I/O
头文件 | #include<stdio.h> |
---|---|
函数原型 | char *fgets(char * s, int size, FILE * stream); |
功能 | 从一个文件里面读取一个字节 |
参数 | s:保存读取的内容 |
size:读取的字节数 | |
stream:指定的流指针 | |
返回值 | 成功:读取的字节数失败:NULL 如果文件内容读取完毕,也返回NULL |
头文件 | #include<stdio.h> |
函数原型 | int fputs(const char * s, FILE * stream); |
功能 | 向一个文件写入一串字符 |
参数 | s:要写入的数据 |
stream:指定的流指针 | |
返回值 | 成功:写入的数据的字节数 失败:EOF |
注意:从文件结构体指针stream中读取数据,每次读取一行。读取的数据保存在buf指向的字符数组中,每次最多读取bufsize-1个字符(第bufsize个字符赋’\0’),如果文件中的该行,不足bufsize-1个字符,则读完该行就结束。如若该行(包括最后一个换行符)的字符数超过bufsize-1,则fgets只返回一个不完整的行,但是,缓冲区总是以NULL字符结尾,对fgets的下一次调用会继续读该行。
简单来说gets()的执行逻辑是寻找该输入流的’\n’并将’\n’作为输入结束符,但是若输入流数据超过存储空间大小的话会覆盖掉超出部分的内存数据,因此gets()函数十分容易造成缓冲区的溢出,不推荐使用。而fgets()函数的第二个参数指定了一次读取的最大字符数量。当fgets()读取到’\n’或已经读取了size-1个字符后就会返回,并在整个读到的数据后面添加’\0’作为字符串结束符。因此fgets()的读取大小保证了不会造成缓冲区溢出,但是也意味着fgets()函数可能不会读取到完整的一行(即可能无法读取该行的结束符’\n’)。
//向文件里面输出
FILE *fp = NULL;
if((fp = fopen(argv[1], “w”)) == NULL)
{
perror(“fail to fopen!”);
return -1;
}
char s[32] = “111111111\n2222222222\n33333333\n”;
fputs(s, fp);
//fputs指针指向文件末尾, fgets获取的数据是文件末尾之后的的数据 也就是没有数据
char s2[32] = {0};
while(fgets(s2, 5, fp) != NULL)
{
printf(“s2 = %s\n”, s2);
}
直接I/O(二进制IO)
在文件流被打开之后,可对文件流按指定大小为单位进行读写操作
fread和fwrite用于读写记录,这里的记录是指一串固定长度的字节,比如一个int、一个结构体或者一个定长数组。参数size指出一条记录的长度,而nmemb指出要读或写多少条记录,这些记录在ptr所指的内存空间中连续存放,共占size * nmemb个字节,fread从文件stream中读出size * nmemb个字节保存到ptr中,而fwrite把ptr中的size * nmemb个字节写到文件stream中。
头文件 | #include<stdio.h> |
---|---|
函数原型 | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); |
功能 | 从流中读一条记录 |
参数 | ptr:存放读入记录的缓冲区 |
size:每个对象的大小(sizeof(int,char…)) | |
nmemb:读取对象个数 | |
stream:指定的流指针 | |
返回值 | 成功:实际读到对象个数 失败:0 |
头文件 | #include<stdio.h> |
函数原型 | size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); |
功能 | 向流中写一条记录 |
参数 | ptr:存放读入记录的缓冲区 |
size:每个对象的大小(sizeof(int,char…)) | |
nmemb:执行一次fwrite函数期望要写的对象个数 | |
stream:指定的流指针 | |
返回值 | 成功:实际写入对象个数 失败:0 |
FILE *fp_r=NULL;
if((fp_r=fopen(argv[1],“r”))==NULL){perror(“fail to opend”);return -1;}
FILE *fp_w=NULL;
if((fp_w=fopen(argv[1],“w”))==NULL){ perror(“fail to opend”);return -1;}
int a[]={1,2,3,4,5,6,7};
printf(“Num write=%d\n”,fwrite(a,sizeof(int),sizeof(a)/sizeof(int),fp_w));
fflush(fp_w);
int b[7]={0};
printf(“Num read=%d\n”,fread(b,sizeof(int),sizeof(a)/sizeof(int),fp_r));
int i=0;
for(i=0;i<7;i++){
printf("%d\n",b[i]);
}
fclose(fp_r);
fclose(fp_w);
注意:写入时fp_w行缓存没有被写满,所以并没有被写入,需要fflush刷新缓存后再进行读操作。并且读时换行符’\n’也算一个固定长度字节
fread()函数和fwrite()函数会将流当做二进制流的形式进行读/写,因此使用fread()/fwrite()操作的文件使用vim打开可能会出现乱码情况。该程序生成的文件是二进制文件而非文本文件,因为其中不仅保存着字符型数据,还保存着整型数据24和28(在od命令的输出中以八进制显示为030和034)
fread()函数结束时,无法自动判断导致fread()函数结束的原因是读取到了文件末尾还是发生了读写错误。这时需要手动判断发生的情况。可以观察最后一次fread()的返回值,或使用feof()/ferror()函数判断。
定位流fseek( )
头文件 | #include<stdio.h> |
---|---|
函数原型 | int fseek(FILE *stream, long offset, int whence); |
功能 | 设置文件偏移量 |
参数 | stream:指定的流指针 |
offset:偏移量(偏移字节数) | |
whence: 相对位置 | SEEK_SET 文件起始位置 |
SEEK_CUR 文件当前位置 | |
SEEK_END 文件末尾位置 | |
返回值 | 成功:0 失败:-1 |
头文件 | #include<stdio.h> |
函数原型 | long ftell(FILE *stream); |
功能 | 获取文件的偏移量 |
参数 | stream:指定的流指针 |
返回值 | 成功:当前文件的偏移量 失败:-1 |
头文件 | #include<stdio.h> |
函数原型 | void rewind(FILE *stream); |
功能 | 定位到文件起始位置 |
参数 | stream:指定的流指针 |
FILE *fp = NULL;
if((fp = fopen(“file.txt”, “w+”)) == NULL)
{perror(“fail to fopen!”);return -1;}
char s1[32] = “123456789”;
fputs(s1,fp);
//获取文件偏移量
printf(“offset = %ld\n”, ftell(fp));
//设置文件偏移量
fseek(fp, -5, SEEK_END);
printf(“offset = %ld\n”, ftell(fp));
//偏移到文件起始位置
rewind(fp);
char s2[32] = {0};
while(fgets(s2, 32, fp) != NULL)
{printf(“s2 = %s\n”, s2);}
fseek(fp, 0, SEEK_SET);
printf(“offset = %ld\n”, ftell(fp));
fputs(“abcde”, fp);
fclose(fp);
fseek重定位流(数据流/文件)上的文件内部位置指针使用fseek()函数可以定位流的读写位置,通过偏移量+基准值的计算将读写位置移动到指定位置,其中第二个参数offset的值为正时表示向后移动,为负时表示向前移动,0表示不动。
fseek函数和lseek函数类似,但lseek返回的是一个off_t数值,而fseek返回的是一个整型。成功,返回0,失败返回-1,并设置errno的值,可以用perror()函数输出错误。
函数 ftell() 用于得到文件位置指针当前位置相对于文件首的偏移字节数。在随机方式存取文件时,由于文件位置频繁的前后移动,程序不容易确定文件的当前位置。使用fseek函数后再调用函数ftell()就能非常容易地确定文件的当前位置。
ftell(fp);利用函数 ftell() 也能方便地知道一个文件的长。如以下语句序列: fseek(fp, 0L,SEEK_END); len =ftell(fp); 首先将文件的当前位置移到文件的末尾,然后调用函数ftell()获得当前位置相对于文件首的位移,该位移值等于文件所含字节数。
void rewind 将文件内部的位置指针重新指向一个流(数据流/文件)的开头
文件I/O
文件io就是系统调用,用户通过应用层函数调用linux内核函数从而控制硬件设备。
不带缓冲
不带缓冲指的是每个read和write都调用内核中的相应系统调用
文件描述符
Linux操作系统是基于文件概念搭建起来的操作系统(“万物皆文件”),基于这一点,所有的I/O设备都可以直接当做文件来处理。因此操作普通文件的操作函数与操作设备文件的操作函数是相同的,这样大大简化了系统对不同设备、不同文件的处理,提高了效率
那么对于内核而言,内核是如何区分不同的文件呢?内核使用文件描述符来索引打开的文件。文件描述符是一个非负整数,每当打开一个存在的文件或创建一个新文件的时候,内核会向进程返回一个文件描述符,当对文件进行相应操作的时候,使用文件描述符作为参数传递给相应的函数。
通常一个进程启动时,都会打开三个流:标准输入、标准输出、标准错误输出,这三个流的文件描述符分别是0、1、2,对应的宏定义是STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。可以查看头文件unistd.h查看相关定义。
内核用以标识一个特定进程正在访问的文件
当读、写一个文件时,用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。
当进程创建或者开启的时候,会自动分配三个文件描述符
0 标准输入 STDIN_FILENO
1 标准输出 STDOUT_FILENO
2 标准出错 STDERR_FILENO
文件I/O使用文件描述符打开操作一个文件,可以访问不同类型的文件(例如普通文件、设备文件和管道文件等)。而标准I/O使用FILE指针来表示一个打开的文件,通常只能访问普通文件。
1、打开文件
open()/creat()
调用open()/creat()函数可以打开或者创建一个文件。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
open()和creat()调用成功返回文件描述符,失败返回-1,并设置errno。
open()/creat()调用返回的文件描述符一定是最小的未用描述符数字。
creat()等价于open(pathname, O_CREAT|O_WRONLY|O_TRUNC, mode)
open()可以打开设备文件,但是不能创建设备文件,设备文件必须使用mknod()创建。
头文件 | #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> |
---|---|
函数原型 | int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); |
功能 | 创建或者打开文件,得到文件描述符 |
参数 | pathname:文件名 |
flags:文件的访问权限 | |
mode:如果flags参数指定为O_CREAT,则必须有这个参数 表示创建的文件的权限,例如0666 | |
返回值 | 成功:文件描述符 失败:-1 |
原型 | int open(const char *pathname, int flags, mode_t mode); | |
---|---|---|
参数 | pathname | 被打开的文件名(可包括路径名)。 |
flags | O_RDONLY:只读方式打开文件。 | 这三个参数互斥 |
O_WRONLY:可写方式打开文件。 | ||
O_RDWR:读写方式打开文件。 | ||
O_CREAT:如果该文件不存在,就创建一个新的文件,并用第三的参数为其设置权限。 | ||
O_EXCL:如果使用O_CREAT时文件存在,则可返回错误消息。这一参数可测试文件是否存在。 | ||
O_APPEND:以添加方式打开文件,所以对文件的写操作都在文件的末尾进行 | ||
O_TRUNC:如文件已经存在,那么打开文件时先删除文件中原有数据。 | ||
O_NOCTTY:使用本参数时,如文件为终端,那么终端不可以作为调用open()系统调用的那个进程的控制终端。 | ||
mode | 被打开文件的存取权限,为8进制表示法。 |
fclose()
头文件 | #include <unistd.h> |
---|---|
函数原型 | int close(int fd); |
功能 | 关闭一个文件描述符 |
参数 | d:指定的文件描述符 |
返回值 | 成功:0 失败:-1 |
标准io和文件io的权限表示
r O_RDONLY
r+ O_RDWR
w O_WRONLY | O_CREAT | O_TRUNC, 0666
w+ O_RDWR | O_CREAT | O_TRUNC, 0666
a O_WRONLY | O_CREAT | O_APPEND, 0666
a+ O_RDWR | O_CREAT | O_APPEND, 0666
read( )
头文件 | #include <unistd.h> |
---|---|
函数原型 | ssize_t read(int fd, void *buf, size_t count); |
功能 | 从文件中读取数据 |
参数 | fd:指定的文件描述符 |
buf:保存读取的数据 | |
count:预计读取的字节数 | |
返回值 | 成功:实际读取的字节数(如果读取到文件末尾,则返回0) 失败:-1 |
write( )
头文件 | #include <unistd.h> |
---|---|
函数原型 | ssize_t write(int fd, const void *buf, size_t count); |
功能 | 向文件写入数据 |
参数 | fd:指定的文件描述符 |
buf:要写的数据 | |
count:要写的字节数 | |
返回值 | 成功:写入文件的字节数 失败:-1 |
int fd_r, fd_w;
//打开源文件
if((fd_r = open(argv[1], O_RDONLY)) < 0)
{perror(“fail to open!”);return -1;}
//打开或者创建目标文件
if((fd_w = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0)
{perror(“fail to open!”);return -1;}
//向目标文件写数据 -cp
ssize_t bytes;
char s[32] = {0};
while((bytes = read(fd_r, s, sizeof(s))) > 0)
{
write(fd_w, s, bytes);
}
printf(“cp done!\n”);
lseek()
每个打开的文件都有一个与其相关的“当前文件位移量”,它是一个非负整数,用以度量从文件开始处计算的字节数。通常,读/写操作都从当前文件位移量处开始,在读/写调用成功后,使位移量增加所读或者所写的字节数。
lseek()调用成功为新的文件位移量,失败返回-1,并设置errno。
lseek()只对常规文件有效,对socket、管道、FIFO等进行lseek()操作失败。
lseek()仅将当前文件的位移量记录在内核中,它并不引起任何I/O操作。
文件位移量可以大于文件的当前长度,在这种情况下,对该文件的写操作会延长文件,并形成空洞。
头文件 | #include <sys/types.h> #include <unistd.h> |
---|---|
函数原型 | off_t lseek(int fd, off_t offset, int whence); |
功能 | 设置或者获取文件的偏移量 |
参数 | fd:指定的文件描述符 |
offset:设置的偏移量 | |
whence: 相对位置 | SEEK_SET 文件起始位置 |
SEEK_CUR 当前文件位置 | |
SEEK_END 文件末尾位置 | |
返回值 | 成功:当前文件的偏移量 失败:-1 |
/如果以之追加的方式,就算设置偏移量,也只能在文件末尾进行追加
两种I/O比较
I/O模型 | 文件I/O | 标准I/O |
---|---|---|
缓冲方式 | 非缓冲I/O | 缓冲I/O |
操作对象 | 文件描述符 | 流(FILE *) |
打开 | open() | fopen()/freopen()/fdopen() |
读 | read() | fread()/fgetc()/fgets()… |
写 | write() | fwrite()/fputc()/fputs()… |
定位 | lseek() | fseek()/ftell()/rewind() |
关闭 | close() | fclose() |
获取文件属性
我们可以使用stat()/fstat()/lstat()函数来获取某个文件的属性信息。
注意:stat既是Linux系统的用于查看文件属性的指令,又是在编程过程中可以使用的一个获取文件属性信息的函数。
其中stat()函数可以根据文件名(可带路径)获取文件的属性信息;fstat()函数可以根据已打开文件的文件描述符获得该文件的属性信息;lstat()函数用法类似于stat(),不过若该文件是一个符号链接文件则会返回该符号链接的信息而不是该符号链接的引用文件的信息。
函数stat()/fstat()/lstat()
需要头文件:#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
函数原型:int stat(const char *pathname,struct stat *buf);
int fstat(int fd,struct stat *buf);
int lstat(const char *pathname,struct stat *buf);
函数参数:pathname:打开文件名(可以包含具体路径名)
fd:已打开文件的文件描述符
buf:struct stat类型的结构体指针,用于存放文件信息
函数返回值:成功:0
失败:-1
struct stat结构体成员
{
dev_t st_dev; /* ID of device containing file - 文件所在的设备ID*/
ino_t st_ino; /* inode number - inode节点号*/
mode_t st_mode; /* protection - 文件的模式(文件、目录等)*/
nlink_t st_nlink; /* number of hard links - 链接至此文件的链接数(硬链接)*/
uid_t st_uid; /* user ID of owner - 文件所有者ID*/
gid_t st_gid; /* group ID of owner - 文件组ID*/
dev_t st_rdev; /* device ID (if special file) - 设备号(针对某些特殊设备文件)*/
off_t st_size; /* total size, in bytes - 文件大小(单位字节)*/
blksize_t st_blksize; /* blocksize for file system I/O - 系统块大小*/
blkcnt_t st_blocks; /* number of 512B blocks allocated - 文件所占的块数*/
time_t st_atime; /* time of last access - 最近被访问时间(例如使用read()读取数据)*/
time_t st_mtime; /* time of last modification - 最近被修改时间(例如使用write()写入数据)*/
time_t st_ctime; /* time of last status change - 文件状态改变时间*/
};
其中文件的类型存放在结构体成员st_mode内
S_ISREG(m) is it a regular file?
S_ISDIR(m) directory?
S_ISCHR(m) character device?
S_ISBLK(m) block device?
S_ISFIFO(m) FIFO (named pipe)?
S_ISLNK(m) symbolic link? (Not in POSIX.1-1996.)
S_ISSOCK(m) socket? (Not in POSIX.1-1996.)
例:
int ret;
struct stat *buf = (struct stat *)malloc(sizeof(struct stat));
ret = stat("./b.c",buf);
if(ret == -1)
{perror(“fail to stat”);exit(1);}
if(S_ISREG(buf->st_mode))
printf("-\n");
if(S_IWUSR & buf->st_mode)//000010 & 11111000(按位与)
printf(“w\n”);
创建buf时可用struct stat buf使用取地址&buf传参
操作目录相关函数
我们可以使用opendir()/readdir()函数来打开某目录并获取目录内文件的信息。在这里opendir()函数相当于fopen()函数,readdir()函数相当于fread()函数,只不过操作的流的结构体类型不同。
1.int mkdir(const char *pathname, mode_t mode);//dd
功能:创建一个目录
参数:pathname:目录的路径名
mode:目录的权限
返回值:成功返回0,失败返回-1
2.DIR *opendir(const char *name);//dd
功能:获得目录流
参数:要打开的目录
返回值:成功:目录流 失败:NULL
该函数的返回值是操作目录的流DIR的指针,其中操作目录的流DIR的结构体成员如下:
struct __dirstream
{
void __fd; / struct hurd_fd’ pointer for descriptor. */
char __data; / Directory block. */
int __entry_data; /* Entry number `__data’ corresponds to. */
char __ptr; / Current pointer into the block. */
int __entry_ptr; /* Entry number `__ptr’ corresponds to. */
size_t __allocation;/* Space allocated for the block. */
size_t __size; /* Total valid data in the block. */
__libc_lock_define (, __lock) /* Mutex lock for this structure. */
};
typedef struct __dirstream DIR;
3.struct dirent *readdir(DIR *dirp);
功能:读目录
参数:要读的目录流
返回值:成功返回结构体指针,失败返回NULL
STRUCT DIRENT {
INO_T D_INO; /* INODE NUMBER */
OFF_T D_OFF; /* NOT AN OFFSET; SEE NOTES */
UNSIGNED SHORT D_RECLEN; /* LENGTH OF THIS RECORD */
UNSIGNED CHAR D_TYPE; /* TYPE OF FILE; NOT SUPPORTED
BY ALL FILESYSTEM TYPES */
CHAR D_NAME[256]; /* FILENAME */
};
#include <dirent.h>
4.int closedir(DIR *dirp);
功能:关闭目录流
参数:目录流
返回值:成功0 失败-1
例:实现ls功能
mkdir(“test”,0777);
//DIR *opendir(const char *name);
DIR *dir=NULL;
dir=opendir("."); //当前目录
if(dir==NULL){
perror(“fail to opendir”);
exit(1);
}
//struct dirent *readdir(DIR *dirp);
struct dirent *ret=NULL;
while((ret=readdir(dir))!=NULL){
if(strcmp(".", ret->d_name)==0 ||strcmp("…",ret->d_name)==0){
continue;
}
printf("%s ",ret->d_name);
}
if(0 != errno){
perror(“error occured”);
return -1;
}
closedir(dir);
库
1.定义:本质上来说库是一种可执行代码的二进制形式;
通俗讲就是把一些常用函数的目标文件打包在一起,提供相应
函数的接口,便于程序员使用;它可以被操作系统载入内存执行。
由于windows和linux的本质不同,因此二者库的二进制是不兼容的。
2.库的分类:
1)静态库:静态库在程序编译时会被连接到目标代码中,
程序运行时将不再需要该静态库,因此体积较大。
制作步骤:
1-将源文件编译生成目标文件
gcc -c hello.c -o hello.o
2-创建静态库用ar命令,它将很多.o转换成.a
ar是类似gcc的一个GNU工具包内的工具,作用是建立、修改、提取归档文件。归档文件是包含多个文件内容的一个大文件,被包含文件的原始内容、权限、时间戳、所有者等属性都保存于归档文件中,并且可以通过“提取”来还原该文件。
ar参数解析:
1.c**:表示无提示方式创建文件包**
2.r**:在文件包中替代文件**
3.s**:强制重新生成文件包的符号表**
ar crs libmyhello.a hello.o
(libmyhello.a静态库文件名)
静态库文件名的命名规范是以lib为前缀,
紧接着跟静态库名,扩展名为.a。
3-使用静态链接库
gcc main.c -L. -lmyhello (myhello库名)
-L 指定库的路径
-l 指定库的名字
4-执行./a.out
补充:$nm 静态库文件名:查看静态库里面包含的目标文件
优点:
程序中已包含代码,运行时不再需要静态库。
运行时无需加载库,运行速度更快。
缺点:
静态库中的代码复制到了程序中,
使程序会占更多的磁盘和内存空间
注:若需要将函数形式及形参参数等提供给他人,需要将.h文件一起提供。若不需要则可以编译到静态库中。
2)动态库:
动态库在程序编译时并不会被连接到
目标代码中,而是在程序运行时才被载入,
因此在程序运行时还需要动态库存在,因此代码体积较小。
制作步骤:
1-我们用gcc来创建共享库
gcc -fPIC -c hello.c -o hello.o
-fPIC 创建与地址无关的编译程序
gcc -shared -o libmyhello.so.1 hello.o
-fPIC(或-fpic):表示编译为位置独立的代码。位置独立的代码即位置无关代码,在可执行程序加载的时候可以存放在内存内的任何位置。若不使用该选项则编译后的代码是位置相关的代码,在可执行程序加载时是通过代码拷贝的方式来满足不同的进程的需要,没有实现真正意义上的位置共享。
shared:指定生成动态链接库
2-生成库对应的符号链接(这一步可以省略,
省略的话上面库文件名以.so结尾)
ln –s libmyhello.so.1 libmyhello.so
3-编译代码
gcc main.c -L. -lmyhello
gcc参数解析:
1.-L**:表示增加目录,让编译器可以在该目录下寻找库文件。后面的 . 表示当前目录**;
2.-l**:表示加载libXXX.a/libXXX.so库文件**。
4-为了让执行程序顺利找到动态库,有三种方法 :
(1)把库拷贝到/usr/lib和/lib目录下。
注意:注意权限问题。
(2)将库的路径添加到环境变量中
export LD_LIBRARY_PATH=/home/linux/IO_process/1902/day_3/share/
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
(终端关闭,环境变量就没在了)
我的:
export LD_LIBRARY_PATH=/home/linux/IO/share
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
Linux****系统还无法定位到我们自己制作的库的位置,即我们暂时还无法使用该动态库。
对于Linux系统而言,在可执行程序加载动态库的时候,不仅要知道该库的名字,还需要知道其绝对路径。
我们可以使用ldd指令查看某个可执行程序加载库的情况
ldd hello
优缺点:
优点: 程序在执行时加载动态库,代码体积小
将一些程序升级变得简单。
不同的应用程序如果调用相同的库,那么在内存
里只需要有一份该共享库的实例。
缺点:运行时还需要动态库的存在,移植性较差