Linux系统编程——文件篇

应用为王

文件编程,要深究到背后的理论原理,内容就太多了,至少包括:

  • 文件系统原理、
  • 文件访问机制、
  • 文件在内核中的管理机制、
  • 文件信息节点inode
  • 文件共享
  • 文件权限,以及权限的修改
  • ……
    但在实际应用中,比如帐单打印、游戏进度保存、配置文件存储等,我们更关心的是内容和正确无误地创建、保存、修改这些文件,而不是操作系统具体是怎么操作这些文件的。

此教程以应用为主,更加关心如何用代码操作文件,实现文件的创建、打开、编辑等自动化执行

至于具体实现原理,待入门应用掌握后,接触到内核操作再做研究

文档操作的基本流程

无论在Linux还是Windows,文档操作的流程无非都是打开/创建文件->编辑文件->保存文件->关闭文件

在Linux中,文件操作提供了一系列的API,包括:

  • 打开文件:open
  • 读写文件:read/write
  • 光标定位:lseek
  • 关闭文件:close

文件操作原理简介

  1. 在Linux中要操作一个文件,一般是先open打开一个文件,得到文件描述符,然后对文件进行读写操作(或其他操作),最后是close关闭文件即可。
  2. 文件读写操作完成后,一定要关闭文件,否则会造成文件损坏。
  3. 文件平时是存放在块设备中的文件系统文件中的,我们把这种文件叫静态文件,当我们去open打开一个文件时,linux内核做的操作包括:
  • 内核在进程中建立一个打开文件的结构体,记录下我们打开的这个文件,结构体的内容包括文件描述符、buf(内容缓存区)、信息节点;
  • 内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内核中特定地址管理存放(叫动态文件)。
  1. 打开文件以后,以后对这个文件的读写操作,都是针对内存中的这一份动态文件的,而并不是针对静态文件的。
    当然我们对动态文件进行读写以后,此时内存中动态文件和块没备文件中的静态文件就不同步了,当我们close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新(同步)块设备中的静态文件。
  2. 为什么这么设计,不直接对块设备直接操作?块设备本身读写非常不灵活,是按块读写的,而内存是按字节单位操作的,而且可以随机操作,很灵活。

open方法——文件打开/创建

头文件:

1
2
3
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
`man open`的输出

如上图所示为open方法的手册,可以看到open主要作用为给定文件名,如果文件存在返回一个非负的文件描述符,文件描述符可用于对指定文件进行操作,包括:readwritelseekfcntl等。如果文件不存在,会返回-1。

关于文件描述符

文件描述符,简单来说作用就是文件的“指针”或“索引”,假如打开了多个文件,操作文件时如何确定是具体哪一个文件?就是通过文件描述符。Linux下,打开文件时,系统会创建一个结构体来管理这些打开的文档,描述符就相当于是结构体数组的“下标”

文件描述符原理简介:

  1. 对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或者创建一个新文件时,内核向进程返回一个文件描述符。当读写一个文件时,用open和creat返回的文件描述符标识该文件,将其作为参数传递给read和write。

    按照惯例,UNX shell使用文件描述符0与进程的标准输入(比如键盘)相结合,文件描述符1与标准输出(比如当前命令行)相结合,文件描述符2与标准错误输出相结合。STINFILENO、STDOUT_FILENO、STDERR_FILENO这几个宏代替了0、1、2这几个魔数。
  2. 文件描述符,这个数字在一个进程中表示一个特定含义,当我们open一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护的这个动态文件的这些数据结构绑定上了,以后我们应用程序如果要操作这个动态文件,只需要用这个文件描述符区分。
  3. 文件描述符的作用域就是当前进程,出了这个进程文件描述符就没有意义了。open函数打开文件,打开成功返回一个文件描述符,打开失败,返回-1。

下面这段代码中不含fd,借助标准输入和标准输出也能调用read与write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
char readBuf[128];
int n_read = read(0, readBuf,5);
int n_write = write(1,readBuf,strlen(readBuf));
printf("\n");

return 0;
}

与open相关的函数有两个:open()、creat()

open()参数说明

如上,open函数有两个必选参数pathname和flags,一个可选参数mode

其中,pathname为string类型的文件名,flags代表打开方式,mode表示文件权限

flags可选择O_READONLY、O_WRONLY、O_RDWR,分别代表只读打开、只写打开、可写可读打开

除了上面三个选项必选一个,flags选项还可叠加下面的可选项,包括:

  • O_CREAT,若文件不存在则新建,需和mode参数搭配以表明新创建文件的权限
  • O_EXCL,若同时指定了O_CREATE且文件不存在,则会出错
  • O_APPEND,每次修改文件,以追加方式写到文件末尾
  • O_TRUNC,打开文件时如果这个文件本来有内容,且为只读或只写成功打开,则清除原有内容

flags参数可叠加,比如:

open(“./file1.txt”,O_RDWR|O_CREAT,00600)可以先尝试打开,如果文件不存在,以600的权限创建该文件

关于mode参数

mode参数代表创建模式,即创建后用户对该文件的权限,是一个5位的整数,前两位固定为0,后三位分别代表当前用户权限、用户组权限和其他用户权限。对于当前用户权限,常见的有三种取值:

  • S_IRUSR/00400表示当前用户可读(r);
  • S_IWUSR/00200表示当前用户可写(w);
  • S_IXUSR/00100表示当前用户执行(x);
  • S_IRWXU/00700表示可读可写可执行。

create()——文件创建

1
int create(const char* filenmame, mode_t mode)

与open类似,不细说

write方法

write方法用于写入指定文件描述符,如果写成功,返回size_t类型的写入内容长度,如果发生错误则返回-1

头文件:#include<unistd.h>

调用方法:write(int fd,const char* buffer,size_t count)其中,count表示写入内容的大小,对指针不能用sizeof。如果是字符串的话就用strlen(头文件_string.h_)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <Stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd;
const char* str="Linux Programming is cool";

fd = open("./file1",O\_RDWR|O\_CREAT|O\_EXCEL,00666);
if(fd == -1){
printf("file exists!\n");
exit(-1);
}

write(fd,str,strlen(str));
close(fd);
return 0;
}

关于 size_t

count参数,和write函数的返回值都是size_t类型的,表示的是指定和实际执行的字节数。size_t类型可理解为无符号整型数,但与unsigned int略有不同。

read方法——读取文件

lseek——光标定位

顾名思义,光标定位用于解决读写文件时对操作位置的寻址定位。lseek函数调用格式:lseek(int fd, int offset, int whence)以whence为起点,该函数将光标移动offset偏移量距离,offset为正则向后,为负则向前

whence三种取值(宏定义量,非字符串)

  • SEEK_SET,文件开头
  • SEEK_CURRENT,当前位置
  • SEEK_END,文件末尾

巧妙计算文件大小(字节数)

将光标移动到文件末尾,lseek函数的返回值就是文件大小:int filesize = lseek(fd, 0, SEEK_END);

关于内容覆盖

使用open()函数打开文件时,如果flags参数除三选一外不加其他选项,则默认写操作从文件头开始,写入内容会依次覆盖掉原来位置的内容,长度不够则剩余部分内容保存(换行也算一个字符,尽管不显示)。

小应用 1 ——用open与write实现简单的cp命令(文本文件)

实现思路:使用open打开指定源文件和目标文件(TRUNC方式打开,强制清空内容),利用read命令读取源文件内容,再利用write命令将读到的内容写进目标文件,最后关闭两个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
int fdSrc;
int fdDes;

char *readBuf=NULL;
if(argc != 3){
printf("pararm error, usage: mycp sourcefile targetfile \n");
exit(-1);
}

fdSrc = open(argv[1],O_RDWR);
if(fdSrc==-1){
printf("source file does not exist or you don't have the access to this file\n");
exit(-2);
}
int size = lseek(fdSrc,0,SEEK_END);
lseek(fdSrc,0,SEEK_SET);

readBuf=(char *)malloc(sizeof(char)*size + 8);

int bytes_read = read(fdSrc, readBuf, size);

fdDes = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0700);

int bytes_write = write(fdDes,readBuf,strlen(readBuf));

if(bytes_read!=bytes_write){
printf("error occured while copying from source file to target file. \n");
}
else{
printf("copy success!\n");
}
close(fdSrc);
close(fdDes);
return 0;
}

小应用 2 -修改配置文件

形如下面格式的配置文件,如何修改特定位置的参数,比如clientid设置为100?

1
2
appid=26
clientid=35

关键是要找到“clientid”这个字符串在哪。如果一行一行去读,判断是否有匹配位置,太慢了

幸好,Linux为我们提供了一个工具:strstr()函数。该函数可快速返回主串中模式串首次出现的位置

如上图,该函数调用格式为strstr(const char* haystack, const char* needle),两个参数分别代表主串和模式串

read与write函数读取/操作数据格式的问题

目前为止,我们操作文件的内容,都是字符串,那是不是说只能写入字符型?

并非如此。write() 函数的第二个参数是const void*类型,也就是说只要是一个指针类型,指向写入内容缓冲区的地址,就可以。

下面就是一个写入结构体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

struct Test
{
int a;
char c;
};

int main()
{
int fd;
struct Test data[2] = {{100,'a'},{101,'b'}};
struct Test data2[2];

fd = open("./file1",O_RDWR);

int n_write = write(fd,&data,sizeof(struct Test)*2);
lseek(fd,0,SEEK_SET);
int n_read = read(fd, &data2, sizeof(struct Test)*2);

printf("read %d,%c \n",data2[0].a,data2[0].c);
printf("read %d,%c \n",data2[1].a,data2[1].c);
close(fd);
return 0;
}

编译运行过后,控制台会输出:

1
2
read 100,a
read 101,b

这表明我们不仅能写入结构体,还能读结构体。如果用vi、cat等命令查看file1的内容,会发现对应位置“乱码”,这是由于操作命令默认读取内容都是字符类型造成的,并不影响程序去读取、控制这些数据。

C库标准文件读取操作函数——fopen、fcreate、fwrite的引入

这三个与前面学习的open、write、create的对比

以下内容来自文章《总结open与fopen的区别》,作者idiot-marker

  1. 来源不同,open是Unix系统(包括Linux等)调用函数,而fopen是ANSIC标准的C语言库函数,在不同的系统中应调用不同的内核api
  2. 返回值不同,open返回的是文件描述符表示文件在文件描述符表里的索引,fopen返回指向文件结构的指针。
  3. 移植性,从前面来源也能看出,fopen是C标准函数,因此具有良好的移植性,open函数是Unix系统调用,别的平台移植性有限
  4. 适用范围方面,由于Unix系统下一切皆以文件形式操作,包括普通正规文件、网络套接字、硬件设备、标准输入和输出等,而fopen函数只适用于操作普通正规文件
  5. 文件IO所处层次,open方法运行在内核态,属于低级IO,而fopen方法是运行在用户态的高级IO
  6. 缓冲与非缓冲。
  • 缓冲文件系统指在内存开辟一个“缓冲区”,文件读写时都需要先经过内存“缓冲区”,并且写操作时需等“缓冲区”装满后再写入文件,“缓冲区”越大,操作外存的次数越少,文件操作效率就越高。涉及函数包括fopen, fclose, fread, fwrite, fgetc, fgets, fputc, fputs, freopen, fseek, ftell, rewind
  • 非缓冲文件系统指借助文件结构体来对文件进行管理,通过文件指针(系统操作用指针,调用open等函数时用描述符),涉及函数有:open, close, read, write, getc, getchar, putc, putchar。这是系统级的输入输出,速度快效率高,但若考虑移植性则慎用

总而言之,open方法运行在内核态,无缓冲区,文件操作时涉及从内核态到用户态的切换;fopen方法运行在用户态,有缓冲区,文件操作时减少了用户态和内核态的切换。效率方面表现为:如果顺序访问文件,fopen系列函数快于open系列函数,如果随机访问文件则相反。

标准C库下,打开、创建、读写文件,以及光标移动操作引入

基本操作与open系列函数类似,此处不再叙述。看下面的小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>

int main()
{
//FILE *fopen(const char *path, const char *mode);

FILE *fp;
char *str = "LP is so cool";
char readBuf[128] = {0};//初始化缓存内容防止末尾无关内容的写入

fp = fopen("./file1","w+");//mode参数用字符串表示

//size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);//size表示每次写的大小,nmemb表示写的次数,stream为文件指针
fwrite(str,sizeof(char),strlen(str),fp);//可以选择一个字符一个字符往里写,总共写strlen(str)次,也可选择一次性写入strlen(str)个字符
//fwrite(str,sizeof(char)*strlen(str),1,fp);
fseek(fp,0,SEEK_SET);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
fread(readBuf,sizeof(char),strlen(str),fp);

printf("read data: %s\n",readBuf);

return 0;
}

需要注意一点,当往文件中写内容时,假设缓存区有20字符,要求写入100字符,则就会写入100字符,最终函数返回值取决于nmemb;反过来,文件只有20字符时,要求读100字符,只能读到20字符。另外,mode参数可选值如下:

  • r 只读方式打开一个文本文件
  • rb 只读方式打开一个二进制文件
  • w 只写方式打开一个文本文件
  • wb 只写方式打开一个二进制文件
  • a 追加方式打开一个文本文件
  • ab 追加方式打开一个二进制文件
  • r+ 可读可写方式打开一个文本文件
  • rb+ 可读可写方式打开一个二进制文件
  • w+ 可读可写方式创建一个文本文件
  • wb+ 可读可写方式生成一个二进制文件
  • a+ 可读可写追加方式打开一个文本文件
  • ab+ 可读可写方式追加一个二进制文件

标准C库写入结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <string.h>
#include <stdlib.h>
struct Test
{
int a;
char c;
};

int main()
{
FILE *fp;
struct Test data = {100,'a'};
struct Test data2;

fp = fopen("./file1","w+");
int n_write = fwrite(&data,sizeof(struct Test),1,fp);

fseek(fp,0,SEEK_SET);

int n_read = fread(&data2, sizeof(struct Test), 1,fp);
printf("read %d,%c \n",data2.a,data2.c);
fclose(fp);
return 0;
}

fgetc、fputc、feof

详细讲解参见fgetcfputc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <string.h>

int main()
{
FILE *fp;
int i;
char *str = "Lp cool!";
int len = strlen(str);

fp = fopen("./test.txt","w+");
for(i=0;i<len;i++){

fputc(*str,fp);
str++;
}
fclose(fp);
return 0;
}

至此,Linux环境下文件编程操作入门学习完毕,至于具体更深入的原理学习、应用实战,随课程深入会继续练习


Linux系统编程——文件篇
https://dockingyuan.top/2022/09/15/LinuxProgramming/File/
作者
Yuan Yuan
发布于
2022年9月15日
许可协议