Linux高级编程之文件I/O

一、文件I/O

1、文件IO与标准IO

1.1、什么是文件I/O?

​ 文件IO就是直接调用内核提供的系统调用函数。

1.2、什么是标准I/O?

​ 标准IO就是间接调用系统调用函数,是C库函数。

1.3、文件IO和标准IO的区别

Linux高级编程之文件I/O_第1张图片

文件IO是直接调用内核提供的系统调用函数,头文件是unistd.h,标准IO是间接调用系统调用函数,头文件是stdio.h,文件IO是依赖于Linux操作系统的,标准IO是不依赖操作系统的,所以在任何操作系统下,使用标准IO,也就是C库函数操作文件的方法都是相同的。

2、文件IO相关系统调用函数

2.1 文件描述符

​ 文件描述符(file descriptor)通常是一个小的非负整数内核用以标识一个特定进程正在访问的文件; 当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符。 在读、写文件时,可以使用这个文件描述符。

​ 当我们对一个文件做读写操作的时候,我们是用open函数返回的这个文件描述符会标识该文件,并将其作为参数传递给read或者write函数。

标准输入、标准输出和标准出错

​ 按惯例,每当运行一个新程序时,所有的shell都为其打开3个文件描述符,即标准输入(stdin)标准输出(stdout)以及标准出错(stderr) 分别对应0, 1, 2 ;对于这些描述符,可以通过重定向例如标准输出,将其重定向到一个文件中去,那么输出的内容便不会在终端显示,而是写入到文件中去。

对于标准输入/输出/出错 的符号常量
STDIN_FILENO, STDOUT_FILENO 和 STDERR_FILENO

如图 显示了 一个进程对应的3张表之间的关系。 该进程有两个不同的打开文件:0 stdin 1 stdout

Linux高级编程之文件I/O_第2张图片

如果两个独立进程各自打开了同一个文件,则有如图所示的关系

Linux高级编程之文件I/O_第3张图片

进程表项、文件表项、节点表项

每个进程在进程表中都有一个记录项,进程表项中包含有一张打开文件描述符表

1、文件描述符表

(1)记录了文件的文件描述符标志

(2)指向一个文件表项的指针

2、内核为所有打开的的文件都维持一张文件表

文件表项包含:

(1)文件状态标志(读、写、填写、同步和非阻塞等)

(2)当前文件偏移量

(3)指向v节点的指针

3、v节点表

v节点包含了文件类型和对此文件进行各种操作的函数的指针

对于大多数文件,v节点还包含了该文件的i节点,i节点包含了文件的所有者、文件长度、文件所在设备、指向文件实际数据块在磁盘上所在位置的指针等

(linux系统只使用i节点,而不使用v节点)

原子操作

追加到一个文件 O_APPEND是一个原子操作;

例如A、B两个进程同时对文件进行写入,如上图两个进程都有一个文件表项,但是共享V节点表项。

假定进程A调用了lseek,它将进程A的该文件偏移量设置为1500字节(当前文件尾端处)。然后内核切换进程,进程B运行。进程B也使用lseek定位到1500字节(当前文件尾端处),由于进程调度,A写入文件100字节,将A的文件表项中的当前文件偏移量更新至1600,此时将V节点的当前文件长度更新至1600。进程调度B运行执行与A相同的操作,这便会导致将A进程写入的数据给覆盖掉。

打开文件时设置O_APPEND标志。 这样做使得内核在每次写操作之前,都将进程的当前文件偏移量设置到该文件的尾端处,于是在每次写之前就不再需要调用lseek。

char *str1 = "hello world";
char * str ="11122222";
source_fd = open(argv[1], O_RDWR | O_APPEND);   //以O_APPEND标志打开
write(source_fd, str1, strlen(str1));			//写入数据
lseek(source_fd, 0, SEEK_SET);					//将文件偏移量定位到文件开始
read(source_fd, buf, 5);						//读取5个字节,此时文件偏移量为5字节
write(source_fd, str, strlen(str));				//再次写入数据
lseek(source_fd, 0, SEEK_SET);				
read(source_fd, buf, 512);						//读取

这里可以看到,使用O_APPEDN之后再次写入直接从文件的末尾开始写入,而不是从当前文件偏移量的位置写入

在这里插入图片描述

2.2 open函数

函数原型

   #include 
   #include 
   #include 
   
   int open(const char *pathname, int flags);  
   
   int open(const char *pathname, int flags, mode_t mode);

pathname: 文件路径

flags 对打开的文件操作的选项 当使用多个选项是使用 “或”操作 例如 O_CREAT | O_RDWR

mode 创建文件时的权限,会与~umask进行与操作 (mode和umask异或操作)例如默认情况下umask为022 当我们创建一个文件传递的权限为0777时,那么实际的权限为~022 & 0777 = 0755 也就是所谓的屏蔽掉了相应的权限

关于mode参数, 需要使用O_CREAT才能使其生效

r – 4 w-- 2 x–1

flags 功能
O_WRONLY 只写打开
O_RDONLY 只读打开
O_RDWR 读写打开
O_EXEC 只执行打开
O_APPEND 每次写时都追加到文件的末尾(默认打开是从文件开始位置处写入) 当以write操作时,都会自动将文件偏移量自动定位到文件的末尾
O_CREAT 如果不存在则创建,使用此选项,需要使用mode参数,但是如果想要得到需要的权限需要对umask进行修改
O_EXCL 如果同时指定了O_CREAT,而文件已存在,则出错;可以用来测试该文件是否存在
O_NOCTTY 如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端,如串口设备
O_TRUNC 如果此文件存在,并且为 只写读写方式打开,则将其长度截断为0
O_NONBLOCK或O_NDELAY 此选项为文件的本次打开操作和后续的I/O操作设置为 非阻塞方式, 不使用该参数默认为阻塞模式,最好使用O_NONBLOCK选项而不是O_NDELAY选项,因为后者的读操作返回值具有二义性,例不能从管道、FIFO或设备中读得数据,则返回0 ,而与读到文件尾返回0 冲突

返回值 成功 0 失败返回 -1

2.3 close函数

关闭一个打开的文件

#include 

   int close(int fd);

关闭一个文件时还会释放该进程加在该文件上的所有记录锁。

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

2.4 read 函数

   #include 

   ssize_t read(int fd, void *buf, size_t count);  // ssize_t  实际上就是int  带符号的整型数字

如read成功返回,则返回读到的字节数,如已到达文件的尾端,则返回0 , 失败则 返回 - 1

​ 有多种情况可实际读到的字节数少于要求读的字节数。

​ (1)读普通文件时,在读取到要求字节数之前已经到达了文件尾端; 例如 文件尾端之前只有30个字节,而要求读取100个字节,那么read返回30,下一次再次调用时返回0

​ (2) 当从终端设备读取时,通常一次最多读一行。

​ (3) 当从网络读取时,网络中的缓冲机制可能造成返回值小于所要求的字节数。

​ (4) 当从管道或FIFO读取时,若管道包含的字节少于所需的字节数,那么返回实际可用的字节数

​ (5) 当一信号造成中断,而已经读了部分数据量时。 读操作从文件的当前偏移量初开始,在成功返回钱,该偏移量将增加实际读到的字节数

工程中的read函数写法

do{				
	ret = read(fd, buff, BUF_SIZE);					
}while(ret == -1 && errno == EINTR);	
if (-1 == ret)  
{
	perror("read");
	return -1;
}

2.5 write函数

   #include 

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

​ 向打开的文件写入数据。

成功返回 实际写入的字节数, 失败返回 -1

其返回值通常与参数count的值相同,否则表示出错。 write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。

例如 完成cp命令


#include
#include 
#include 
#include 
int main(int argc,char *argv[])
{
	if (3 != argc)
	{
		printf("无效的参数\n");
		printf("./cp file_a file_b\n");
	}
	int source_fd, target_fd;
	char buf[1024] = {'\0'};
	int ret = 1;
	source_fd = open(argv[1], O_RDONLY);
	if (source_fd == -1){
		perror("open");
		return -1;
	}
	target_fd = open(argv[2], O_CREAT | O_RDWR, 0777);
	if (target_fd == -1)
	{
		perror("open");
		return -1;
	}
	while(ret)
	{
		do{
			ret = read(source_fd, buf, sizeof(buf));
		}while(ret == -1 && errno == EINTR);
		
		if (ret == -1)
		{
			perror("read");
			return -1;
		}
		ret = write(target_fd, buf, ret);
		if (ret == -1)
		{
			perror("write");
			return -1;
		}
	}
	return 0;
}

2.6 lseek 函数

​ 每当打开文件都有一个与其相关的“当前文件偏移量”(current file offset)。 它通常是一个非负整数,用以度量从文件开始出计算的字节数。 通常,读、写操作都是从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置0

   #include 
   #include 

   off_t lseek(int fd, off_t offset, int whence);

对于offset的解释与参数whence的值有关

whence offset
SEEK_SET 将该文件偏移量设置为距文件开始处的offset个字节 只能为正数
SEEK_CUR 将该文件的偏移量设置为其当前值加offset, offset可正可负
SEEK_END 将改文件偏移量设置为文件长度加offsetoffset可正可负

Linux高级编程之文件I/O_第4张图片

示例:

  lseek(fd , 0 , SEEK_SET); //文件开始位置
  lseek(fd , 0 , SEEK_END); //文件末尾位置
  lseek(fd , 10 , SEEK_END); //从文件末尾往后移动10个字节
  lseek(fd , -10 , SEEK_END); //从文件末尾往前移动10个字节
  lseek(fd , 100 , SEEK_SET); //从文件开始往后移动100个字节

注意:

  1. 如果文件偏移量往回超出文件头位置,则返回-1,文件指针不变,还是处于原来的位置。

  2. lseek并不适用与所有的文件类型,也就是说在管道,FIFO,socket或终端不能使用lseek函数,一旦调用将会失败,并设置errno为EPIPE。

  3. 通常,文件的当前偏移量应当是一个非负整数,但是,某些设备 也可能允许负的偏移量; 对于普通文件,其偏移量必须是非负值。因为某些设备的偏移量可能是负值,所以在比较lseek的返回值时应当谨慎,不要测试是否小于0,而要测试是否等于-1

4. lseek仅将当前的文件偏移量记录在内核中,它并不引起任何的I/O操作。 然后,该偏移量用于下一个读或写操作。

5. 文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都将被读为0

 ```
 char * str ="11122222";
 ret = lseek(source_fd, 30, SEEK_END);
 if (ret == -1)
 {
 	perror("lseek");
 	return -1;
 }
 write(source_fd, str, strlen(str));
 ```

Linux高级编程之文件I/O_第5张图片

2.7 文件截断

​ 有时我们需要在文件尾端处截去一些数据以缩短文件。将一个文件的长度截断为0是一个特例,在打开文件时使用O_TRUNC标志可以做到这一点。为了截断文件可以调用函数truncate() 和 ftruncate()

       #include 
       #include 

       int truncate(const char *path, off_t length);
       int ftruncate(int fd, off_t length);

这两个函数将一个现有文件长度截断为length。 如果该文件以前的长度大于length,则超过length以外的数据就不能访问。如果以前的长度小于length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)。

但是,如果说之前文件中有数据,但是当重新写入的数据小于原有数据时,那么便会发生此时的数据后面包含未被覆盖的数据

例如连续之前写入了hello worldhello world hello worldhello world;

当此时写入hello worldhello world 再次读取时,便会将未覆盖的数据也会输出

解决办法1、 首先可以使用O_TRUNC 参数,但是,当以只写或读写方式打开的时候,便清空数据

那么再读取原有数据时,便会无数据可读

解决办法2

使用ftruncate函数

ftruncate(fd, length);

2.8 dup/dup2 复制文件描述符

函数原型

   #include 

   int dup(int oldfd);
   int dup2(int oldfd, int newfd);

由dup返回的新文件描述符一定是当前可用文件描述符中的最小值。对于dup2,可以用newfd参数来指定新的文件描述符的值。如果newfd已经打开,则先将其关闭。若oldfd == newfd, 则dup2返回newfd,而不关闭它。

dup/dup2主要是用来进行文件描述符重定向,例如将 stdin或stdout 重定向到文件
Linux高级编程之文件I/O_第6张图片

例如将标准输出重定向到文件中

#include
#include 
#include 
int main(int argc,char *argv[])
{
	int oldfd, newfd;
	oldfd = open("stdout_file", O_CREAT | O_RDWR, 0777);
//	close(STDOUT_FILENO);  //关闭标准输出
//	newfd = dup(oldfd);	//将标准输出重定向到stdout_file
	newfd = dup2(oldfd, STDOUT_FILENO);	  //将标准输出重定向到文件 先关闭标准输出
	printf("hello world\n");
	return 0;
}

2.9 fcntl函数

#include 
#include 

int fcntl(int fd, int cmd, ... /* arg */ );
			成功,返回值依赖于cmd  失败返回-1

fcntl函数有以下5种功能

(1) 复制一个已有的文件描述符(cmd = F_DUPFD 或者 F_DUPFD_CLOEXEC)

(2) 获取/设置文件描述符标志 (cmd = F_GETFD 或 F_SETFD)。

(3) 获取/设置文件状态标志(cmd = F_GETFL 或 F_SETOWN)。

(4) 获取/设置异步I/O所有权 (cmd = F_GETOWN 或 F_SETOWN)。

(5) 获取/设置记录锁 (cmd = F_GETLK、 F_SETLK 或 F_SETLKW)。

这里先说明复制文件描述符操作,后面的在学习相应操作时再补充

cmd 功能
F_DUPFD 复制文件描述符fd。新文件描述符作为函数值返回。他是尚未打开的描述符中大于等于第3个参数值当前未用的最小的文件描述符。 但是,新的文件描述符有自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志会被清除 0 (这表示该描述符在exec时仍保持有效)
F_DUPFD_CLOEXEC 复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新的文件描述符 与F_DUPFD的区别是 前者会将FD_CLOEXEC设置为0 而后者会复制和被复制的文件描述符相同的FD_CLOEXEC值
F_GETFD 对应于fd的_文件描述符标志_作为**函数值返回****。当前只定义了一个文件描述符标志FD_CLOEXEC 默认值为0
F_SETFD 对于fd设置文件描述符标志。新标志值按第3个参数(取为整数值)设置

关于 执行时关闭close-on-exec标志默认值为0 (系统默认,在exec时不关闭) 值为1(在exec时关闭该文件)

2.10 close-on-exec

关于close-on-exec标志

dup/dup2 和 fcntl使用F_DUPFD标志 获取的新的文件描述符 都会将close-on-exec标志清空

fcntl 使用F_DUPFD_CLEXEC参数复制旧的描述符的close-on-exec标志


	oldfd = open("stdout_file", O_CREAT | O_RDWR, 0777);
	
	flags = fcntl(oldfd, F_GETFD);  //获取标志  默认close-on-exec为关闭状态
	flags |= FD_CLOEXEC;  //打开close-on-exec 
	fcntl(oldfd, F_SETFD, flags);  // 设置新的flags
	flags = fcntl(oldfd, F_GETFD);  //获取标志  默认close-on-exec为关闭状态
	
	dupfd2 = dup2(oldfd, 4);	//复制文件描述符从4开始
	flags = fcntl(dupfd2, F_GETFD);  //获取标志 dup2返回的新的描述符清空close-on-exec标志位
	
	dupfd = dup(oldfd);	
	flags = fcntl(dupfd, F_GETFD);  //获取标志 dup返回的新的描述符清空close-on-exec标志位

	newfd = fcntl(oldfd, F_DUPFD, 4);  //新的文件描述符大于等于4
	flags = fcntl(newfd, F_GETFD);  //获取标志 返回的新的描述符清空close-on-exec标志位
	newfd = fcntl(newfd, F_DUPFD_CLOEXEC);
	flags = fcntl(newfd, F_GETFD);  //获取标志 返回的新的描述符复制旧的描述符的lose-on-exec标志位

Linux高级编程之文件I/O_第7张图片

2.11 文件属性

首先来查看文件属性结构体 struct stat

struct stat {
	mode_t st_mode;		//文件类型及权限  
	ino_t     st_ino;    	 //inode值
    dev_t    st_rdev;   	//设备号
    nlink_t   st_nlink;  	//硬链接设备数
    uid_t     st_uid;     //用户ID
    gid_t     st_gid;     //用户组ID
    off_t     st_size;     //文件大小
    struct timespace   st_atime; 	//最后一次访问时间
    struct timespace   st_mtime;	//最后一次修改时间
    struct timespace   st_ctime;  	//最后一次改变属性时间
    long      st_blksize;   //数据块大小
    long      st_blocks;   //数据块数量
};
struct timespace
{
    time_t tv_sec;  //秒
    long   tv_nsec; // 纳秒
}
2.11.1文件类型

(1) 普通文件(regular file)。 这是最常用的文件类型,这种文件包含了某种形式的数据。至于这种数据是文本还是二进制数据对于UNIX内核并无区别。

**-**rw-r–r-- 1 root root 77 4月 2 21:03 test

(2) 目录文件(directory file)。 这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。 对于一个目录文件具有读权限的任意一个进程都可以读该目录的内容,但只有内核可以直接写目录文件。 进程必须使用目录相关的函数才能更改目录

drwxr-xr-x 2 root root 60 4月 4 14:18 lightnvm

(3) 块设备文件(block special file)。这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定字节长度为单位进行。

brw-rw---- 1 root disk 8, 1 4月 4 14:18 sda1

(4) 字符设备文件(character special file)。这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符设备,要么是块设备文件

crw-rw---- 1 root dialout 4, 85 4月 4 14:18 ttyS21

(5) FIFO 有名管道 ,这种类型的文件用于进程间通信。

prwxr-xr-x 1 root root 0 9月 28 2020 FIFO

(6) 套接字文件(socket) 这种类型的文件用于进程间的网络通信。

srwxrwxrwx 1 mysql mysql 0 04-19 11:12 /var/lib/mysql/mysql.sock

(7) 符号链接(symbolic link)。这种类型的文件指向另一个文件

lrwxrwxrwx 1 root root 15 4月 4 14:18 stdin -> /proc/self/fd/0

2.11.2 文件类型的宏
文件类型
S_ISREG() 普通文件
S_ISDIR() 目录文件
S_ISCHR() 字符设备文件
S_ISBLK() 块设备文件
S_ISFIFO() 管道或FIFO
S_ISLINK() 符号链接文件
S_ISSOCK() 套接字
2.11.3 stat函数

获取当前文件的信息;需要使用绝对路径

   #include 
   #include 
   #include 

   int stat(const char *pathname, struct stat *statbuf);
   int fstat(int fd, struct stat *statbuf);
   int lstat(const char *pathname, struct stat *statbuf);

lstat 函数,返回改符号连接的有关信息,而不是由该符号链接引用的文件的信息; 当以降序遍历目录层次结构时,需要使用lstat

struct stat *statbuf = malloc(sizeof(stat));

struct stat statbuf;
stat("/home/nfs", &statbuf);
printf("%ld\n",statbuf->st_ino);
/*************************************************************************
    > File Name: stat.c
    > 作者:YJK
    > Mail: [email protected]
    > Created Time: 2021年04月28日 星期三 09时18分21秒
 ************************************************************************/

#include
#include 
#include 
#include 
int main(int argc,char *argv[])
{
	int i;
	struct stat buf;
	char *ptr;
	for (i = 1; i < argc; i ++)
	{
		printf("%s\n", argv[i]);
		if (lstat(argv[i], &buf) < 0)
		{
			perror("lstat");
			continue;
		}
		if (S_ISREG(buf.st_mode)) // 普通文件
			ptr = "regular";
		else if (S_ISDIR(buf.st_mode)) // 目录文件
			ptr = "directory";
		else if (S_ISCHR(buf.st_mode)) // 字符设备文件
			ptr = "character special";
		else if (S_ISBLK(buf.st_mode)) // 块设备文件
			ptr = "block special";
		else if (S_ISFIFO(buf.st_mode)) // 管道或 FIFO文件
			ptr = "fifo";
		else if (S_ISLNK(buf.st_mode)) // 链接文件
			ptr= "link";
		else if (S_ISSOCK(buf.st_mode)) // 套接字
			ptr = "socket";
		else
			ptr = "unknown mode";

		printf("%s\n", ptr);
	}
	return 0;
}

2.12 umask函数

   #include 
   #include 

   mode_t umask(mode_t mask);
		返回值: 之前的文件模式创建屏蔽字

umask 函数为进程设置文件模式创建屏蔽字,并返回之前的值。

只改变当前进程的umask 并不改变系统的umask

对于umask命令来说, 改变的是当前终端的umask

你可能感兴趣的:(Linux高级编程,linux)