Linux系统编程_文件I/O_标题重复率高,建议修改提高曝光

不定期补充、修正、更新;欢迎大家讨论和指正

概览

一般来说,我们操作文件的方式有打开文件,读文件,写文件等等,内核都会提供系统函数来完成这些操作。
我们知道Linux一切皆文件,所以scanf从键盘输入数据和print打印数据到屏幕,原理也是操作这些特殊文件实现的。
scanf和printf是c库函数,可以想到它们应该是文件I/O进一步的封装。

文件描述符

内核(kernel)利用文件描述符(file descriptor)来访问文件。
打开现存文件或新建文件时,内核会返回一个文件描述符,文件描述符是非负整数。
读写文件也需要使用文件描述符来指定待读写的文件。
所以文件描述符是很基础重要的概念,下面IO函数的参数也需要用到文件描述符

每一个进程都有PCB(进程控制块,在Linux为task_struct),是用来记录进程属性的结构体,而文件描述符地又是PCB内一个结构体
用来记录进程打开的文件。
文件描述符的结构体如图

在这里插入图片描述

在这里插入图片描述

每个进程生成时都默认使用了0,1,2描述符,<unistd.h>定义了宏STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO来代替0,1,2
0,1,2的文件路径在dev/std*
STDIN_FILENO 标准输入 可以理解为鼠标键盘输入
STDOUT_FILENO 标准输出  可以理解屏幕输出
STDERR_FILENO  标准错误输出
需要注意的是,新生成的文件描述符都是当前未使用的数组号中最小那个
如 已经有0.1.2.3描述符 ,通过close关闭0,新open的文件描述符是0而不是4,再open一个才是4.
一般文件描述符从3开始  
同一进程不可能有相同的文件描述符,不同进程可能有相同的文件描述符
如下,关闭了0描述符,fd1为0而不是3

在这里插入图片描述
在这里插入图片描述

Linux 2.4.22 强制规定文件描述符最多不能超过 1,048,576 ,也就是说一个进程可以最多可以同时打开这么多文件,最大值可以改,但没必要。

文件描述符简介

open(2)

NAME
       open, openat, creat - open and possibly create a file

SYNOPSIS

       int open(const char *pathname, int flags);
       //pathname文件路径,flags打开文件的方式,下面有列出,成功返回文件描述符,失败返回-1
       
       int open(const char *pathname, int flags, mode_t mode);//只在使用O_CREAT选项时使用,添加文件八进制文件权限
       														  //如0755,和Linux文件属性一样的表示方法
       														  //也可以用头文件自带的宏定义
       														  //需要注意即使指定0777,实际的创建的文件的权限为0755,
       														  //这是因为还有与umask值取反后的值相与才得到真正的权限值
       int creat(const char *pathname, mode_t mode);
		//由于早期的UNIX版本open的flags参数只能是0,1,2即O_RDONLY,O_WRONLY,O_RDWR
		//不能打开尚未存在的文件,所以需要调用另一系统函数creat创建文件,现在的open函数提供选择项
		//O_CREATE和O_TRUNC,并且creat只能以只写方式打开所创建的文件,所以creat函数就很少使用了
		
       int openat(int dirfd, const char *pathname, int flags);
       int openat(int dirfd, const char *pathname, int flags, mode_t mode);
       //1.如果pathname指定的是绝对路径名。这种情况下,dirfd参数将被忽略,openat函数等同于open函数。
	   //2.如果pathname指定的是相对路径,并且dirfd的值不是AT_FDCWD,pathname则参照dirfd指定的目录下寻找,
       //dirfd可以是当前目录(打开的是当前目录),也可以其他目录。dirfd是通过open函数打开相对路径名所在的目录来获取的。
	   //3.如果pathname指定相对路径名,并且dirfd的值为AT_FDEWD时,相对路径名在当前目录获取(相当于当前目录的相对路径)。
	   
	   //后续很多函数都有at的后缀,意思跟这里类似,at的作用如下
	   //1.让线程可以使用相对路径名打开目录中的文件,不再是只能打开当前目录中的文件。
	   //同一进程中的所有线程共享当前目录,很难让同一进程中的线程工作在不同目录。
	   //2.避免time-of-check-to-time-of-use(TOCTTOU)错误。
	   //如果有两个基于文件的函数调用,并其中一个依赖另一个的结果,这个程序是脆弱的。
	   //这两个调用都不是原子操作,在两个函数调用期间,可能文件发生了变化,进而导致影响最终结果。
	   //文件系统命名空间中的TOCTTOU错误通常处理那些特权程序降低特权文件的权限控制或通过特权文件打开一个安全漏洞等方式进行。
	   
————————————————
原文链接:[open和openat的区别]https://blog.csdn.net/qq_44842973/article/details/103137721

flags参数
		O_RDONLY	只读打开,等价于0 
		O_WRONLY	只写打开,等价于1
		O_RDWR		读写打开,等价于2 
		//以上的三个选项只能指定一个
		O_APPEND 	每次写文件都追加在文件末尾  //同一进程和多个进程多次打开同一文件时,会出现写入相互覆盖的状况
											//这是因为每个文件描述符有自己维护的文件偏移量,初始默认为0
											//这就会导致A写入10个字节A的文件偏移量为10  若B也写入10个字节,因为偏移量从0开始
											//就会覆盖A之前写的记录
											//一个解决的方法就是打开文件时添加O_APPEND选项,当文件状态标志有此选项
											//文件的偏移量就会设置为文件大小,其他进程总会在文件末尾写入
									//[多次打开同一个文件]https://blog.csdn.net/qq_41719582/article/details/104665229
		O_CREAT		若文件不存在则创建,同时说明mode参数。
	
		O_EXCL		若同时指定O_CREAT,而文件已经存在,则出错
		O_TRUNC		若文件存在且为只读或只写成功打开,则将其长度截断为0
		O_NOCTTY	如果打开的文件是终端设备,则不将此设备分配作为进程的终端控制
		O_NONBLOCK	如果打开的文件是FIFO、块特殊文件、字符特殊文件,就会以阻塞的方式等待用户接下来的操作,如
					等待用户输入,若不想阻塞则用此选项变为非阻塞
		O_SYNC		每次write都等到物理I/O操作完成

read(2)

NAME
       read - read from a file descriptor

SYNOPSIS
      
       ssize_t read(int fd, void *buf, size_t count);
	//每次从fd读取count字节的数据写入buf  ssize_t为有符号的Long型数据类型,
	//成功返回读取字节数,即一般情况为count字节,0代表读到文件末尾
	//当返回值小于count,不代表出错。比如一次500读取1200字节的文件,第三次返回值为200
	//亦或者是从管道,网络或终端读取的缘故,read()被信号中断时也会出现返回的字节数小于count值
	//错误返回-1,需要注意当open以O_NONBLOCK(非阻塞)的方式读取一般情况下阻塞的文件,如终端,管道
	//虽然会返回-1,但是我们要的就是非阻塞的效果,这时就需要判断ERRORS是否等于 EAGAIN or EWOULDBLOCK

基本用法


#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#define N 1024
#include<stdlib.h>
int main(int argc,char *argv[])
{
        int fd=open(argv[1],O_RDONLY);
        if(fd==-1){
                perror("open fd");
                exit(1);
        }
        int n=0;
        int  buf[N];

        while( n=read(fd,buf,N)){
                printf("n=%d\n",n);
        }
        close(fd);
        return 0;
}

读取正常的文件,并查看返回值
可以发现,读取的字节数加起来刚好是文件的大小

在这里插入图片描述

write(2)

NAME
       write - write to a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);
       //每次从buf读取count个字节的数据写入fd. ssize_t为有符号的Long型数据类型,
       //成功返回写入的字节数,0代表buf已经没有数据可写入fd.
       //同样的返回值小于count,不代表出错
       //比如磁盘写满亦或者超过一个给定进程文件程度限制
       //失败返回-1

read和write实现文件的复制

思路很简单,每从源文件read()读取多少个字节,就用write()写入新文件多少个字节.
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#define N 512
#include<stdlib.h>
int main(int argc,char *argv[])
{
        int fd1=open(argv[1],O_RDONLY);
        if(fd1==-1){
                perror("open fd1");
                exit(1);
        }
        int fd2=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
        if(fd2==-1){
                perror("open fd2");
                exit(1);
        }
        
        int n=0;
        int  buf[N];

        while( n=read(fd1,buf,N)){
                write(fd2,buf,n);
        }

        close(fd1);
        close(fd2);
        return 0;
}

在这里插入图片描述

fputc()和write()读写一个字节

我们知道fputc()和fgetc()一次只读写一个字节,如果write()和read()的count也取一个字节,那么谁的执行效率快?
直觉告诉我们,fputc()是库函数,write()是系统调用函数,fputc()执行时不能直接调用内核资源,还需要通过
write()函数调用,相比write()直接调用系统资源,fputc()肯定效率比write()低
而事实上write()却比fputc()慢上许多,read()和fgec()同理
现在用strace命令追踪函数每一步的动作
fputs_cp
#include<stdio.h>
#include<stdlib.h>

int main(void){
        FILE *fp1,*fp2;
        int n;

        fp1=fopen("/etc/ssh/ssh_config","r");
        if(fp1==NULL){
                perror("fopen fp1 error");
                exit(1);
        }

        fp2=fopen("./ssh2","w");
        if(fp2==NULL){
                perror("fopen fp2 error");
                exit(1);
        }
        while((n=fgetc(fp1))!=EOF){
                fputc(n,fp2);
        }
        fclose(fp1);
        fclose(fp2);
        return 0;
}

fputs()
strace ./get_cp
注意红色箭头
实际上是4096个字节

在这里插入图片描述

write()
strace ./cp /etc/ssh/ssh_config  ./ssh1
可以看到write()和read()的确是遵循一个一个字节执行

在这里插入图片描述

原因是fputs()函数在用户层有用户进程缓冲区,当用户进程缓冲区写满(如4096)才写入内核的缓冲区
而write()是直接调用内核资源,所以只能一个一个字节写入内核缓冲区。
所以read()、write()函数也被称为Unbuffered I/O无用户级缓冲区。

buf的取值

buf的取值会影响读写的效率,那多少才合适。
这里贴出APUE的结论:8192

在这里插入图片描述

lseek(2)

NAME
       lseek - reposition read/write file offset
       //重定位读写文件的偏移量,每打开文件都有一个相关的当前文件偏移量,用以度量从
       //文件开始处计算的字节数,通常读写操作都是当前文件偏移量开始
       //当打开一个文件时,系统默认偏移量被设置为0,即从文件头开始读写操作
       //可以理解为我们打字时的光标,lseek的作用就是修改光标的位置,然后进行其他操作
       //当用open函数打开文件时指定O_APPEND选项时,偏移量会通过读取文件大小后固定设置在文件末尾

SYNOPSIS
      
       off_t lseek(int fd, off_t offset, int whence);
		//通过whence和offset重定位fd的偏移量
		//成功返回当前文件的偏移量,失败返回-1
		
whence
		SEEK_SET	文件开始处,offset只能为正
		SEEK_CUR	文件当前位置,offset可正可负
		SEEK_END	文件末尾,offset可正可负,若为正,会生成offset个空洞
		//可以理解whence是粗定位,offset是精确定位,通过lseek(fd,0,SEEK_END)的返回值可以获得文件的大小。
f1从文件开头位置偏移10,其返回值应为10
接着写入10个字节,文件偏移量因此加10
f2为当前文件偏移量,由上可知应为20
f3从文件结尾偏移-5,即向前移5个字节,因为当前文件大小为20,f3应为20-5=15(若超过-20则返回-1)

在这里插入图片描述

结果与预期一致,查看文件,write()写入的数据的确也存在。

在这里插入图片描述

空洞文件

当whence设置为SEEK_END,但offset大于0时并不会报错,而是生成空洞文件。只要偏移量大于当前文件大小,就会出现这种情况。
空洞文件并不会实际占用磁盘空间

空洞文件作用很大,例如迅雷下载文件,在未下载完成时就已经占据了全部文件大小的空间,这时候就是空洞文件。下载时如果没有空洞文件,多线程下载时文件就都只能从一个地方写入,这就不是多线程了。如果有了空洞文件,可以从不同的地址写入,就完成了多线程的优势任务

浅析空洞文件

close(2)

NAME
       close - close a file descriptor

SYNOPSIS
      
       int close(int fd);
		//关闭fd文件描述符的文件,成功返回0,失败返回-1,errno设置相应的值
		//关闭一个文件也会释放该进程加载文件上的记录锁
		//当进程终止时,其所有打开的文件由内核自动关闭,所以忘记使用close关闭文件也不会出现太大问题。

关闭STDIN_FILENO对read(0)、scanf()的影响

正常情况下

在这里插入图片描述
在这里插入图片描述

关闭0描述符,即STDIN_FILENO标准输入,程序运行结束,并不能从键盘和鼠标输入数据

在这里插入图片描述

使用scanf(),可以看到程序并没有让用户输入数据,这是因为scanf()下层调用的是read(),所以标准输入关闭了,scanf()也不能正常工作。
同理关闭1,write(1)和printf()也不能正常使用
关闭2,write(2)和perror()也不能正常使用

在这里插入图片描述
在这里插入图片描述

dup(2)和dup2(2)

NAME
       dup, dup2, dup3 - duplicate a file descriptor//复制新的文件描述符

SYNOPSIS
      

       int dup(int oldfd);
       //传入旧的文件描述符,成功返回新生成的文件描述符,新描述符是当前未使用文件描述符中最小的那个,失败-1
       int dup2(int oldfd, int newfd);
	   //于dup不同的地方就是可以自己指定新描述符,如果新描述符已经存在,则关闭再指向旧描述符
dup函数返回的新描述符和旧文件描述符共享一张文件表项
因为共享了文件偏移量,所以向同一文件写入数据时不会发生相互覆盖的情况

在这里插入图片描述

重定向

本来printf打印的内容是输出到屏幕
利用dup2如果新描述符已经存在,则关闭再指向旧描述符的原理,关闭STDOUT_FILENO描述符,
指向要重定向的文件中。

在这里插入图片描述

本该输出到屏幕的内容被重定向到文件中.

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值