锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

第7章 Linux下的文件编程(一)

时间:2023-04-22 19:37:00 2d型位移变送器

很久没发文章了文章了。Linux系统下的文件编程已经整理好了。如果写的时间太长,人会变懒,要坚持。

Linux下面的文件编程

    • 第7章 Linux以下文件编程(1)
      • 7.1 概述
        • 7.1.1 Linux下的系统调用
        • 7.1.2 基本I/O函数
        • 7.1.3 文件描述符
          • 文件指针与文件描述符的区别如下:
            • 1.数据类型不一致
            • 2.本质不同
      • 7.2 基本I/O操作
        • 7.2.1 open函数
          • mode参数说明
          • 例7-1
          • 例 7-2
        • 7.2.2 close函数
        • 7.2.3 write函数
          • 例 7-3
          • 知识补充:
          • 例 7-4
        • 7.2.4 read函数
          • 例 7-5
          • 例 7-6
        • 7.2.5 lseek函数
          • 例 7-7
          • 例 7-8

第7章 Linux以下文件编程(1)

在Linux在操作系统中,一切都被视为文件。Linux目录和设备将被视为特殊文件。这种处理方法的意义体现在从程序设计人员的角度看所有与文件相关的系统调用是完全一样的,其接口也是一致的,使用起来非常方便,程序完全可以像使用文件一样使用磁盘、串行口、打印机及其他硬件设备。本章主要介绍Linux系统下的文件I/O操作,即基于文件描述符的文件操作,包括open、read、write、close、lseek、mkdir、opendir、readdir、rmdir详细说明文件处理函数。

7.1 概述

在Linux文件操作有两种方式,一种是通过调用C语言库函数,另一种是通过系统调用,前者不依赖操作系统,可以知道任何操作系统;后者依赖于操作系统,文件的操作实际上是通过系统核心提供的接口,不同的操作系统核心提供不同的系统调用方法,本章主要介绍Linux以下系统调用。

7.1.1 Linux下系统调用

所谓系统调用是指操作系统提供好给用户程序的一种“特殊”接口,用户程序可以通过这组特殊接口来获得操作系统内核提供的特殊服务。

在Linux在中间,用户程序不能直接访问核心提供的服务。为了更好地保护核心空间,程序的运行空间分为核心空间和用户空间。它们在不同层次上运行,在逻辑上相互隔离,如图所示:

[外链图片存储失败,源站可能有防盗链机制,建议保存图片直接上传(img-h8PN7WpJ-1655306226829)(https://files.mdnice.com/user/22712/a9e75985-cfac-4f71-8f75-cbca7ce26ba9.png)]
在Linux用户编程接口(API)是遵循了在UNIX最流行的应用编程标准-统一编程界面规范(Portable Operating System Interface,POSIX)。POSIX该标准定义了操作系统应为应用程序提供的接口标准IEEE为要在各种UNIX由操作系统上的软件定义的一系列API标准总称。

7.1.2 基本I/O函数

在Linux中,read和write是基本的系统级IO在应用层过程中使用函数read和write读写Linux在文件中,过程将从用户态进入内核态I/O读取文件中的数据。内核态(内核模式)和用户态(用户模式)Linux限制应用程序可执行指令和可访问地址的机制。不允许在用户模式下启动空间过程I/O因此,操作必须通过系统调用进入核模式Linux读取文件。

大多数Linux文件I/O操作5个函数:open ,read,write,lseek以及close。

注:在命令行输入时检查这些函数的帮助 man 2 read(函数名)

7.1.3 文件描述符

对于系统内核,所有打开的文件都被文件描述符引用。当打开现成文件或创建新文件时,内核将返回到过程中。描述符文件描述符是一个非负整数。当过程启动时,系统会自动打开标准输入、标准输出和标准错误输出三个文件,分别对应文件描述符STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,对应值为0、1、2

在C语言中,当过程启动时,系统还自动打开三个文件:标准输入、标准输出和标准错误输出。这三个文件都与终端相关,因此可以在终端上输入和输出,而无需打开终端文件。下表列出了文件描述符和文件指针的区别。

项目 标准输入(键盘) 标准输出(显示器) 标准错误(显示器)
文件指针 stdin stdout stderr
文件描述符 STDIN_FILENO STDOUT_FILENO STDERR_FILENO
文件描述符合对应值 0 1 2
文件指针与文件描述符的区别如下:
1.数据类型不一致

stdin、stdout和stderr类型为FILE*

STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO类型为int

使用stdin函数主要包括:fread、fwrite、fclose等,基本上从f开始

使用STDIN_FILENO的函数有:read、write、close等。

2.本质不同

标准I/O是ANSI C建立标准I/O模型是标准函数包和stdio.h头部文件中的定义具有一定的可移植性。标准I/O默认使用缓冲机制,如调用fopen函数不仅打开一个文件,还建立了一个缓冲区(两个缓冲区将在读写模式下建立),并建立了一个包含文件和缓冲区相关数据的数据结构。当用fwrite当函数将数据写入磁盘时,首先将数据写入流缓冲区。当满足一定条件时,如流缓冲区满或刷新流缓冲,数据将一次发送到核心提供的块缓冲,然后通过块缓冲写入磁盘(双重缓冲)。写入过程是:数据流->流缓冲区->内核缓存->磁盘。

STDIN_FILENO等是文件描述符。I/O一般没有缓冲操作,需要自己创建缓冲区,但在Linux或UNIX在系统中使用称为内核缓冲该技术用于提高效率。读写调用是直接复制核心缓冲区和过程缓冲区的数据unistd.h定义头文件。写入过程如下:数据流->内核缓存->磁盘

每个过程都可以打开多个文件,从而有多个文件描述符,可以打开的文件数量取决于操作系统,Linux每个过程最多可打开1024个文件。您可以在终端上输入命令ulimit -n查看当前操作系统下最多可以打开的文件数量。

7.2 基本/O操作

本节将详细介绍文件打开、读取、写入、关闭以及移动文件读写指针操作。使用文件操作基本函数,必须将相关头文件引入到程序中,作为初学者,可以使用man手册来查看相关的头文件。

7.2.1 open函数

open函数的功能是打开或者创建文件,对文件进行读写操作之前都需要打开文件,所以open是对文件数据进行存取必须完成的系统调用。由于open本身也是shell命令,所以要查看open函数的帮助信息,要输入man 2 open,表示在手册的第二节找到相关信息。下面详细介绍函数所需头文件、函数原型以及函数返回值。

open函数所需头文件:

#includ e 
#include 
#include 

open函数的原型有两种:

第一种为int open(const char *pathname,int flags,mode_t  mode);
第二种为int open(const char *pathname,int flags)。
当使用open创建一个新文件时,使用第一种有三个参数的open函数;当使用open打开一个文件时,使用第二种有两个参数的open函数。

其中,参数pathname是要创建文件或打开文件的路径(绝对路径/相对路径)。
注:这里创建的文件是文本文件。

参数flags用来标识打开方式。由两组符号常数进行或运算构成flags参数(这些常数定义在头文件中)下表列出了构成flags参数的两组符号常数。

第一组(打开方式) 说明 第二组(其他选项) 说明
O_RDONLY 以只读方式打开文件 O_CREAT 需要创建新文件时加上该参数,使用open函数三个参数的原型
O_WRONLY 以只写方式打开文件 O_EXCL 此参数可测试文件是否存在。若文件存在时而使用了O_CREAT和O_EXCL,那么返回值为-1,errno的值为17,对应的错误描述应该是File Exist
O_RDWR 以读写方式打开文件 O_APPEND 每次进行写入操作时,将新内容追加到文件尾部
O_TRUNC 每次进行写入操作时,先将文件内容清空,再将文件指针移到文件头。

上述两组参数之间用”|“符号进行连接,open函数的第三个参数mode是在创建文件时使用的,用来指定文所创建文件的存取权限,其符号常量和八进制值的对应关系如下表。对于mode,可以使用符号常量,也可以使用按位进行或运算来计算权限。

知识补充1

文件权限是针对三类用户进行的,三类用户分别是文件属主(u)、文件属组用户(g)和其他用户(o)。

权限类型分为三种,读®、写(w)、执行(x),对应的加权值为4,2,1。

例如:新建文件的权限设置为642,则表示属主对文件的操作权限为6=4+2,意味着对文件具有读写权限;属组用户对文件的操作权限为4,意味着对文件具有读权限;其他用户对文件的操作权限为1,意味着对文件具有执行权限

mode参数说明
符号常量 说明
S_IRUSR 属主拥有读权限
S_IWUSR 属主拥有写权限
S_IXUSR 属主拥有执行权限
S_IRGRP 属组拥有读权限
S_IWGRP 属组拥有写权限
S_IXGRP 属组拥有执行权限
S_IROTH 其他用户拥有读权限
S_IWOTH 其他用户拥有写权限
S_IXOTH 其他用户拥有执行权限

知识补充2

在使用open函数打开一个文件时,除了设置打开方式外,还要考虑文件本身的存取许可。见例7-1.

open函数返回值:如果调用成功则返回文件描述符,若出错则返回-1.通过返回值可以判断系统调用是否成功,从而实现对程序的逻辑控制。

注:在vim编辑器中按原格式粘贴的办法,在命令行模式下输入: set paste 然后回车粘贴。

例7-1

在当前目录下创建名为open_test的文件,以只读的方式打开,并设置其权限为:属主可读可写可执行,属组用户可读,其他用户无任何权限。

open函数的原型有两种:
第一种为int open(const char *pathname,int flags,mode_t  mode);
第二种为int open(const char *pathname,int flags)。
//1.open函数所需头文件
#includ e <sys/types.h>
#include 
#include 
//2.输入输出头文件
#include

//3.调用open函数创建open_test文件
int main()
{ 
        
	int fd;//4.声明变量fd,保存文件描述符
	fd=open("open_test",O_RDONLY|O_CREAT,740);//在当前目录下创建open_test文本文件
	//如果是open_test.txt就是在源程序同级目录下创建文本文件open_test.txt,可以加后缀标识文件类型
	//5.通过open函数返回值来判断文件创建是否成功
	if(fd==-1)
	{ 
        
		perror("创建失败!");//6.perror函数可以将errno中的错误信息输出
		return -1}
	else
	{ 
        
		printf("创建成功!");
		printf("%d\n",fd);
	}	
		
}

实验步骤:

  • 首先创建一个空的文件夹dotest进行测试,在该文件夹下创建test.c文件
  • 执行gcc test.c -o a.out指令,编译test.c文件,执行gcc命令后,出现a.out的可执行文件
  • 执行./a.out执行可执行文件,然后会创建一个open_test文件夹。
  • 查看文件的权限,ls -l open_test
[zsh@localhost test1]$ ls
test.c
[zsh@localhost test1]$ gcc test.c -o a.out
[zsh@localhost test1]$ ls
a.out  test.c
[zsh@localhost test1]$ ./a.out
创建成功!3
[zsh@localhost test1]$ ls
a.out  open_test  test.c
[zsh@localhost test1]$ ls -l open_test
--wxr--r-T 1 zsh zsh 0 5月  26 18:02 open_test
[zsh@localhost test1]$ 

注:Linux权限分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1nOTCunt-1655306226838)(https://files.mdnice.com/user/22712/8e285059-1fa2-488a-a5bc-c1a525a00901.png)]

例 7-2

使用open函数将上例中创建的open_test以读写的方式打开,若打开成功则输出“打开成功”的字符串,若失败请分析原因。

#includ e <sys/types.h>
#include 
#include 
#include

int main()
{ 
        
    int fd;
    fd=open("open_test",O_RDWR);
    if(fd==-1)
    { 
        
      perrno("打开失败");
      return -1;
    }
    else 
    { 
        
      printf("打开成功%d",fd);
    }
    return 0;
}
[zsh@localhost test1]$ vi open.c
[zsh@localhost test1]$ gcc open.c -o open.out
[zsh@localhost test1]$ ls
a.out  open.c  open.out  open_test  test.c
[zsh@localhost test1]$ ./open.out 
打开失败: Permission denied
#查看打开失败的原因,权限不够,不能读文件夹
[zsh@localhost test1]$ ls -l open_test 
--wxr--r-T 1 zsh zsh 0 526 18:02 open_test
[zsh@localhost test1]$ chmod u+r open_test  #给属主用户添加r读权限
[zsh@localhost test1]$ ./open.out 
打开成功3[zsh@localhost test1]$   #现在打开成功了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uU9n4j2D-1655306226839)(https://files.mdnice.com/user/22712/494109c1-d6f2-4400-bf0b-64e8b1ca0718.png)]

7.2.2 close函数

close函数的功能是关闭一个已经打开的文件。

close函数所需的头文件:#include

close函数的原型:int close(int fd);其中fd是文件描述符。返回值若是0,表示执行成功;返回值若是-1,则表示执行失败。

当一个进程终止时,它所有的打开文件都由内核自动关闭。很多程序都使用这一功能而不显示地用close关闭打开文件。

示例:修改open.c文件

#include 
#include 
#include 
#include
#include //close函数所需的头文件

int main()
{ 
        
    int fd;
    fd=open("open_test",O_RDWR);
    if(fd==-1)
    { 
        
      perror("打开失败");
      return -1;
    }
    else
    { 
        
      printf("打开成功%d",fd);
      int result;
      result=close(fd);//调用关闭函数
      printf("关闭成功%d",result);
    }
    return 0;
}
[zsh@localhost test1]$ gcc open.c -o open.out
[zsh@localhost test1]$ ./open.out
打开成功3关闭成功0[zsh@localhost test1]$ 

7.2.3 write函数

write函数的功能是向已打开的文件写数据。

write函数需要包含的头文件:#include

write函数的原型:

ssize_t   write(int fd,const void*buf,size_t count);

其中,fd为文件描述符。buf表示写入文件的数据缓冲区地址。count表示写入的字节数。

返回值:若调用成功,则返回已经写入的字节数;若失败,返回-1。常见的出错原因是磁盘空间已满或者超过了文件大小的限制。

例 7-3

使用open函数将open_test以读写方式打开后,使用write函数向文件中写入数据。

#include 
#include 
#include 
#include 
#include 
int main()
{ 
        
	int fd;
	fd=open("open_test",O_RDWR);
	if(fd==-1)
	{ 
        
		perror("打开失败!");
		return -1;
	}
	else
	{ 
        
		//使用write函数向文件open_test中写入字符串
		int ssize=write(fd,"hello,myname is Codersong,也叫西里小诸葛",5);
        //int ssize=write(fd,"hello,myname is Codersong,也叫西里小诸葛",40);
		if(ssize==-1)
		{ 
        
			perror("写入失败");
			return -1;
		}
		else
		{ 
        
			printf("写入成功,写入字节:%d",ssize);
		}
	}
	return 0;
}

编译运行的结果:

[zsh@localhost test1]$ vi write_test.c
[zsh@localhost test1]$ gcc write_test.c -o write_test.out
[zsh@localhost test1]$ ls
open.c  open.out  open_test  test.c  test.out  write_test.c  write_test.out
[zsh@localhost test1]$ ./write_test.out
写入成功,写入字节:5  #写入字节5,所以超出5字节的内容没有写入
[zsh@localhost test1]$ ls
open.c  open.out  open_test  test.c  test.out  write_test.c  write_test.out
[zsh@localhost test1]$ cat open_test
hello[zsh@localhost test1]$ ./write_test.out
写入成功,写入字节:5
[zsh@localhost test1]$ cat open_test   #第二次执行还是只显示一行的hello,说明是覆盖了原文件
hello
#修改写入字节数
[zsh@localhost test1]$ ./write_test.out
写入成功,写入字节:50[zsh@localhost test1]$ 
[zsh@localhost test1]$ cat open_test
hello,myname is Codersong,也叫西里小诸葛

编译上述源程序,执行后,使用vi(或者cat)查看open_test的内容是hello…西里小诸葛

思考:若运行n次后,open_test的内容是什么?是一个hello还是n个hello?

解读:这个思考实际上是看写入的方式是追加还是覆盖。经过测试:默认写入方式是覆盖。

知识补充:

用open函数打开文件后,对文件后续的读和写操作都是从文件指针所指向的位置开始的,每次打开文件后,文件指针默认指向文件头,所以上例中每次的写入操作都是从头开始,因此无论运行多少次,open_test内容都是最后一次写入的"hello"字符串。

如果希望将每次写入的内容追加到文件尾,则需要使用open函数中的O_APPEND。将函数调用语句改成:

fd=open("open_test",O_RDWR|O_APPEND);

重新对更改后的源文件进行编译、链接,运行后查看open_test为"hellohello",实现了将内容追加到文件尾的目的。

如果希望每次写入内容前清空文件内容,则需要使用open函数中的O_TRUNC。将函数调用语句改成:

fd=open(“open_test”,O_RDWR|O_TRUNC);

[zsh@localhost test1]$ cat open_test
hello
[zsh@localhost test1]$ ./write_test.out
写入成功,写入字节:5
[zsh@localhost test1]$ cat open_test
hellohello
例 7-4

编写程序,实现在当前目录下创建10个文件,文件名为1.dat,2.dat,…,10.dat,并将整数1,2,3…10分别写入文件。

#include 
#include 
#include 
#include 
#include 

int main()
{ 
        
	int fd,i;
	char filename[10];
	for(i=1;i<=10;i++)
	{ 
        
		sprintf(filename,"%d.dat",i);
		fd=open(filename,O_RDWR|O_CREAT,644);
		if(fd==-1)
		{ 
        
			perror("创建失败\n");
			return -1;
		}
		int ssize=write(fd,&i,sizeof(int));
		if(ssize==-1)
		{ 
        
			perror("写入失败!");
			return -1;
		}
	}
}

注:查看二进制文件可使用od命令。例如:od 1.dat

本例中的sprintf函数的功能是把格式化的数据写入字符数组。执行结果如下:

[zsh@localhost test1]$ gcc test2.c -o test2.out #编译源代码
[zsh@localhost test1]$ ls
open.c  open.out  open_test  test2.c  test2.out  test.c  test.out  write_test.c  write_test.out
[zsh@localhost test1]$ ./test2.out #执行可执行程序
[zsh@localhost test1]$ ls  #此时多了10个.dat文件
10.dat  2.dat  4.dat  6.dat  8.dat  open.c    open_test  test2.out  test.out      write_test.out
1.dat   3.dat  5.dat  7.dat  9.dat  open.out  test2.c    test.c     write_test.c
[zsh@localhost test1]$ od 1.dat
od: 1.dat: 权限不够
[zsh@localhost test1]$ ls -l 1.dat
--w----r-T 1 zsh zsh 4 527 00:23 1.dat
[zsh@localhost test1]$ chmod u+r 1.dat  #权限不够给文件属主添加读权限
[zsh@localhost test1]$ od 1.dat
0000000 000001 000000
0000004
[zsh@localhost test1]$ chmod u+r 2.dat
[zsh@localhost test1]$ od 2.dat
0000000 000002 000000
0000004

7.2.4 read函数

read函数的功能是向已打开的文件读取数据。

read函数需要包含的头文件:#include

read函数的原型:

ssize_t read(int fd, void *buf, size_t count);

其中,fd为文件描述符;buf表示读出数据缓冲区地址;count表示读出的字节数。

返回值:若调用成功,则返回读到的字节数;若失败,返回-1;若已经达到文件尾,则返回0.因此读到的字节数可能小于count的值。

例 7-5

将例7-4中文件的数据读出并输出到屏幕上。

#include 
#include 
#include 
#include 
#include 

int main()
{ 
        
	int fd,i,j;
	char filename[10];
	for(i=1;i<=10;i++)
	{ 
        
		sprintf(filename,"%d.dat",i); //把格式化的数据写入字符数组
		fd=open(filename,O_RDWR);
		if(fd==-1)
		{ 
        
			perror("打开文件失败\n");
			return -1;
		}
		read(fd,&j,sizeof(int));
        printf("%d\n",j);
	}
}

结果:

[zsh@localhost test1]$ vi read_test.c
[zsh@localhost test1]$ gcc read_test.c -o read_test.out
[zsh@localhost test1]$ ./read_test.out
1
2
打开文件失败
: Permission denied
[zsh@localhost test1]$ chmod u+r ./*.dat
[zsh@localhost test1]$ ./read_test.out
1 2 3 4 5 6 7 8 9 10 
例 7-6

将例7-4中的文件数值加1

#include 
#include 
#include 
#include 
#include 

int main()
{ 
        
	int fd,i,j;
	char filename[10];
	for(i=1;i<=10;i++)
	{ 
        
		sprintf(filename,"%d.dat",i); //把格式化的数据写入字符数组,字符数组存储了10个文件名
		fd=open(filename,O_RDWR);
		if(fd==-1)
		{ 
        
			perror("打开文件失败\n");
			return -1;
		}
		read(fd,&j,sizeof(int));//读取文件的数据存放在变量j中。
        j++;
        write(fd,&j,sizeof(int));
	}
    return 0;
}
#结果
[zsh@localhost test1]$ vi read_test.c
[zsh@localhost test1]$ gcc read_test.c -o read_test.out
[zsh@localhost test1]$ ./read_test.out
2 3 4 5 6 7 8 9 10 11 

程序运行后,用od命令查看.dat文件的内容发现,变量j的值追加到文件尾,而不是覆盖原先的值。具体如下所示:

[zsh@localhost test1]$ od 2.dat
0000000 000002 000000 000003 000000
0000010

原因是read函数被调用后,文件指针也从文件头向右偏移了读取的字节数(这里是一个整型所占的字节数),那么调用write函数时,写入操作是从当前文件指针位置开始的,因此.dat文件中就出现了两个数据,如何解决这个问题呢,请见7.2.5 lseek函数

总结文件描述符fd就是描述文件状态的,fd实际上是一个int值,我们根据文件描述符fd对文件进行读写改操作,不同的返回值就反应了操作的正确与否。

7.2.5 lseek函数

所有打开的文件都有一个当前文件的偏移量(又叫文件指针),文件指针通常是一个非负整数,用于表示文件开始处到文件当前位置的字节数。因此,文件偏移量以字节为单位。

对文件的读写操作开始于文件偏移量,并且使文件偏移量增大,增量为读写的字节数。文件被打开时,文件偏移量为0,即文件指针指向文件头(在打开时使用O_APPEND除外)。

lseek函数的功能就是设置偏移量的值,即可以改变文件指针的位置,从而实现对文件的随机存取。

lseek函数所需头文件:

#include

#include

lseek函数的原型:off_t lseek(int fd, off_t offset, int whence);

其中,fd为文件描述符。

offset:偏移量,指的是每一读写操作所需移动距离,以字节为单位,可正可负,正值表示向文件尾方向移动,负值表示向文件首方向移动。

whence:当前位置的基点,主要有以下三个基点符号常量,通常前两个基点用得比较多。

SEEK_SET,将该文件的位移量设置为距文件开始处offset个字节。

SEEK_CUR,将该文件的位移量设置为其当前值加offset,offset可为正或负。

SEEK_END,将该文件的位移量设置为文件长度offset,offset可为正或为负。

那么例7-6出现的问题就可以使用lseek函数移动指针的方法来解决。代码如下:

#include 
#include 
#include 
#include 
#include 

int main()
{ 
        
	int fd,i,j;
	char filename[10];
	for(i=1;i<=10;i++)
	{ 
        
		sprintf(filename,"%d.dat",i); //把格式化的数据写入字符数组,字符数组存储了10个文件名
		fd=open(filename,O_RDWR);
		if(fd==-1)
		{ 
        
			perror("打开文件失败\n");
			return -1;
		}
		read(fd,&j,sizeof(int));//读取文件的数据存放在变量j中。
        j++;
        //或者lseek(fd,-4,SEEK_CUR)

相关文章