【Linux】 拿下 系统 基础文件操作!!!

简介: 怎么样,我们的猜测没有问题!!!所以语言层的文件操作函数,本质底层是对系统调用的封装!通过不同标志位的封装来体现w r a+等不同打开类型!我们在使用文件操作时,一般都要使用语言层的系统调用,来保证代码的可移植性。因为不同系统的系统调用可以会不一样!

送给大家一句话:

要相信,所有的不美好都是为了迎接美好,所有的困难都会为努力让道。

—— 简蔓《巧克力色微凉青春》

开始理解基础 IO 吧!

1 前言

在C语言已经掌握文件操作的一些接口,接下来我们来从操作系统的层面来理解文件操作!!!

基础IO的篇章我们将讲解以下内容:

  1. 复习C文件IO相关操作
  2. 认识文件相关系统调用接口
  3. 认识文件描述符,理解重定向
  4. 对比fd和FILE,理解系统调用和库函数的关系
  5. 理解文件系统中inode的概念
  6. 认识软硬链接,对比区别
  7. 认识动态静态库,学会结合gcc选项,制作动静态库

2 知识回顾

C语言中要进行文件操作,就一定要先打开这个文件:fopen(),并用一个文件指针来接收

例如:FILE* fp = fopen("log.txt","w")

打开也有可能会失败,所以还要检查fp是否为空指针。当我们使用完文件之后一定一定要关闭文件:fclose(fp)


我们要进行文件操作,前提是我们的程序跑起来了!文件打开和关闭,是CPU在执行我们的程序。所以可以推断出来:

  • 打开文件的本质是进程打开文件!!!
  • 文件没有被打开的时候,文件在哪里??? 当然是磁盘了
  • 进程可以打开多个文件!!! (系统中同样可以存在多个进程) 很多的情况下,OS内部存在大量被打开的文件!那操作系统就要对打开的文件进行管理!! (那么每个打开的文件在OS内部都存在一个对应描述文件属性的结构体,类似PCB)
  • 文件 = 属性 + 内容
  • 注意文件写入方式:
r   Open text file for reading.
  The stream is positioned at the beginning of the file.
  
r+  Open for reading and writing.
    The stream is positioned at the beginning of the file.
    
w   Truncate(缩短) file to zero length or create text file for writing.
    The stream is positioned at the beginning of the file.
    
w+  Open for reading and writing.
  The file is created if it does not exist, otherwise it is truncated.
  The stream is positioned at the beginning of the file.
  
a   Open for appending (writing at end of file).
    The file is created if it does not exist.
    The stream is positioned at the end of the file.
    
a+  Open for reading and appending (writing at end of file).
    The file is created if it does not exist. The initial file position
    for reading is at the beginning of the file,
    but output is always appended to the end of the file.

3 理解文件

3.1 进程和文件的关系

3.2 文件的系统调用

文件 fd 值

我们先来认识一下文件fd:

#include<sys/types.h>  
  2 #include<sys/stat.h>  
  3 #include<stdio.h>  
  4 #include<fcntl.h>  
  5 #include<string.h>  
  6   
  7 int main()  
  8 {  
  9   int fda = open("loga.txt",O_WRONLY|O_CREAT|O_TRUNC);  
 10   printf("fda: %d\n",fda);  
 11   
 12   int fdb = open("logb.txt",O_WRONLY|O_CREAT|O_TRUNC);  
 13   printf("fda: %d\n",fdb);  
 14     
 15   int fdc = open("logc.txt",O_WRONLY|O_CREAT|O_TRUNC);  
 16   printf("fda: %d\n",fdc) ;   
 17   return 0
 18 }  
#include<sys/types.h>

来看效果:

每个文件都有对应的不同的fd值(类似进程的 pid),为什么是从3开始的呢,因为0 1 2 ,都是已经设置好的文件:

  • 0 : 标准输入 – 键盘
  • 1 : 标准输出 – 显示器
  • 2 : 标准错误 – 显示器

在语言层(比如C语言),也会默认打开这三个(stdin stdout stderr)。我们使用文件对应的fd值,也可以实现写入操作了。

那么在操作系统内部,是如何实现的呢?

文件的管理类似进程管理,会有对应的struct filel来进行管理,而且多个进程可能打开同一个文件,所以进程里也一定有管理该进程打开的文件的结构体(struct files_struct),里面包含一个指针数组,每个指针都指向对应的文件,数组的下标也就是每个文件的fd值,所以上层的系统调用使用fd值(数组下标)就能访问对应文件!!!

这时也就可以总结一下open系统调用做了哪些事情:

  1. 创建file
  2. 开辟文件缓冲区的空间,加载文件数据
  3. 查进程的文件描述符表(struct files_struct *file)
  4. file 地址,填入对应表的下表中
  5. 返回下标(fd)

open

各个语言的文件接口基本都是不一样的,那么语言之间有没有共性呢???

我们来看一个系统调用:open(),我们先认识使用一下:

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

第一个参数和C语言fopen的第一个参数一致(表示文件路径或当前目录下的文件名)

第二个参数表示想怎样打开文件,传入的是标志位

第三个参数表示创建文件的权限

会返回一个数字表示是否打开成功。

  1 #include<sys/types.h>  
  2 #include<sys/stat.h>  
  3 #include<stdio.h>  
  4 #include<fcntl.h>  
  5   
  6 int main()  
  7 {  // 以只读形式打开 , 文件不存在就创建
  8   int fd = open("log.txt",O_WRONLY|O_CREAT);  
  9   if(fd < 0)  
 10   {  
 11     perror("open");  
 12     return 1;  
 13   }                                                                                                                                                                           
 14 }   

运行代码:

可以看的我们成功创建了一个新文件,但是文件的权限好像不对(这里因为我们没有设置对应权限,所以是乱码!)

所以才有了第三个参数,来帮助我们确定权限

int fd = open("log.txt",O_WRONLY|O_CREAT,0666); 我们在来看看

哎呦???怎么权限还是不对,我们设置的是666应该是rw-rw-rw-啊???这是因为创建的文件会收到文件掩码的影响:

所以会出现这样的情况,那怎么解决呢?

我们可以使用umask()系统调用,动态修改掩码值(只在该进程中起作用),来达到我们预期的结果:

  1 #include<sys/types.h>
  2 #include<sys/stat.h>
  3 #include<stdio.h>
  4 #include<fcntl.h>
  5 
  6 int main()
  7 {
  8   umask(0);                                                                                                                                                                   
  9   int fd = open("log.txt",O_WRONLY|O_CREAT,0666);                                                             
 10   if(fd < 0)                                                                                                  
 11   {                                                                                                           
 12     perror("open");                                                                                           
 13     return 1;                                                                                                 
 14   }                                                                                                           
 15 } 

这样就创建出来我们预期的效果了!!!


我们再来看看 flag 标志位,它是一个32位的整数,每个比特位代表一个对应功能(OS常用的系统调用接口的常用方法),也就是位图!!!每个宏定义都是一个对应比特位设置为1,想要实现多个功能就进行 | 按位与操作就可以了!!!

常用的标志位参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
上面三个常量,必须指定一个且只能指定一个

O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写

write

接下来我们写入来试试。

  1 #include<sys/types.h>
  2 #include<sys/stat.h>
  3 #include<stdio.h>
  4 #include<fcntl.h>
  5 
  6 int main()
  7 {
  8   umask(0);
  9   int fd = open("log.txt",O_WRONLY|O_CREAT,0666);
 10   if(fd < 0)
 11   {
 12     perror("open");
 13     return 1;
 14   }
 15 
 16   const char* message = "hello linux file!\n";
 17   write(fd , message,strlen(message));
 18 
 19   close(fd);
 20   return 0;                                                                                                                                                                   
 21 }    

这样就成功写入进去了(注意没有写入\0哦)

但是我们在写入aaaa时会发现,原本文件并没有清空,也就是open默认不会清空文件!!!,这时我们加入新的标记位O_TRUNC,就能打开文件就清空了!现在就不会出现叠加的情况了!通过不同的标识位可以做到不同功能(比如追加写入)

read

我们先创建一个文件:

  1 #include<stdio.h>  
  2 #include<sys/types.h>  
  3 #include<sys/stat.h>  
  4 #include<fcntl.h>  
  5 #include<unistd.h>  
  6 #include<string.h>  
  7 
  8 int main()
  9 {
 10   int fd = open("log.txt",O_WRONLY | O_CREAT |O_TRUNC);
 11   if(fd < 0) 
 12   {
 13     perror("open");
 14     return -1;
 15   }
 16 
 17   const char* message = "Hello linux !\n";
 18                                                                                    
 19   write(fd,message,strlen(message));
 20   write(fd,message,strlen(message));
 21   write(fd,message,strlen(message));
 22   write(fd,message,strlen(message));
 23                                        
 24   close(fd);                           
 25   return 0;
 26  
 27 }

这样运行起来我们就可以创建一个文件,并写入相应信息!来看效果

那么我们打开也是很简单将标识符换为,也可以获取对应文件描述符,然后通过使用read函数,我们就可以完成读取文件的操作。

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

其作用是通过指定文件描述符 ,将文件内容读入到 buf 中,读取的个数为 count 字节。

我们在来认识一下系统调用:stat

NAME
       stat, fstat, lstat - get file status

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>
       #include <unistd.h>

       int stat(const char *path, struct stat *buf);
       int fstat(int fd, struct stat *buf);
       int lstat(const char *path, struct stat *buf);

他可以帮助我们指定的文件路径(fstat 通过文件描述符),来获取应该结构体struct stat (表示状态),该结构体是一个输出型变量,也就是我们传入我们创建的结构体的指针,这个系统调用可以帮我写入到我们的结构中,其内部包含一下内容:

struct stat {
               dev_t     st_dev;     /* 文件对应设备的ID */
               ino_t     st_ino;     /* 文件的inode */
               mode_t    st_mode;    /* 文件的权限 */
               nlink_t   st_nlink;   /* 文件的连接数 */
               uid_t     st_uid;     /* 文件所属者的ID */
               gid_t     st_gid;     /* 文件所属组的ID */
               dev_t     st_rdev;    /*  设备 ID (如果是特殊文件) */
               off_t     st_size;    /* 文件有多少字节 */
               blksize_t st_blksize; /* blocksize for file system I/O */
               blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
               time_t    st_atime;   /* 上一次访问的时间 */
               time_t    st_mtime;   /* 上一次修改的时间 */
               time_t    st_ctime;   /* 上一次状态更改的时间 */
};

文件我们知道是由内容和属性组成的,那么我们对文件操作就可以分为两种:

  1. 对内容进行操作(read ,write等都是对内容进行操作)
  2. 对属性进行操作(这个stat 就可以帮助我们对属性进行操作)

而今天read系统调用需要的是off_t st_size; /* 文件有多少字节 */ ,有了大小才可以获取文件全部的内容,我们l来操作一下:

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 #include<string.h>
  7 
  8 const char* filename = "log.txt";
  9 
 10 
 11 int main()
 12 {
 13   struct stat st;
 14   int n = stat(filename , &st);
 15   if(n < 0) return 1;
 16 
 17   printf("file size : %lu\n",st.st_size);                                          
 18                                                             
 19   int fd = open(filename,O_RDONLY);                                        
 20   if(fd < 0)                                                               
 21   {                                                                        
 22     perror("open");                                                        
 23     return -1;                                                             
 24   }                                                                        
 25   printf("fd :%d\n",fd);                                                   
 26   close(fd);
 27   return 0;                                                                      
 28  }

这样我们就可以看到文件的大小了!

其他的属性也可以这样获取!!!我们有了size 之后就可以进行文件读取操作了!!!

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 #include<string.h>
  7 
  8 const char* filename = "log.txt";
  9 
 10 
 11 int main()
 12 {
 13   struct stat st;
 14   int n = stat(filename , &st);
 15   if(n < 0) return 1;
 16 
 17   printf("file size : %lu\n",st.st_size);                                          
 18                                                        
 19   int fd = open(filename,O_RDONLY);                                        
 20   if(fd < 0)                                                               
 21   {                                                                        
 22     perror("open");                                                        
 23     return -1;                                                             
 24   }                                                                        
 25   //printf("fd :%d\n",fd);                                                   
 26   //开辟缓冲区 , 多开一个为了储存 ‘\0’ 
 27   char *file_buffer = (char*)malloc(st.st_size + 1);
 28   //read的返回值为成功读取的个数    
 29   n = read(fd , file_buffer , st.st_size);
 30   if(n > 0)
 31   {   
 32     file_buffer[n] = '\0';
 33     printf("%s",file_buffer);
 34   }   
 35   close(fd);    
 36   return 0;                                                              
 37  }

这样成功打印出来了文件内容!!!

这样成功使用read系统调用了!!!

3.3 语言层如何理解

C语言中FILE其实是一个结构体类型,内部一定会封装文件fd!!!来看

#include<sys/types.h>  
  2 #include<sys/stat.h>  
  3 #include<stdio.h>  
  4 #include<fcntl.h>  
  5 #include<string.h>  
  6   
  7 int main()  
  8 {  
  9   
 10   printf("stdin->fd: %d\n",stdin->_fileno);  
 11   printf("stdin->fd: %d\n",stdout->_fileno);  
 12   printf("stdin->fd: %d\n",stderr->_fileno);  
 13   
 14   FILE* fp = fopen("log.txt","w");  
 15   if(fp == NULL) return 1;  
 16   printf("fd : %d\n",fp->_fileno);  
 17   
 18   FILE* fp1 = fopen("log1.txt","w");  
 19   if(fp1 == NULL) return 1;  
 20   printf("fd : %d\n",fp1->_fileno);  
 21   
 22   FILE* fp2 = fopen("log2.txt","w");  
 23   if(fp2 == NULL) return 1;  
 24   printf("fd : %d\n",fp2->_fileno);    
 25   return 0
 26 }

来看运行效果:

怎么样,我们的猜测没有问题!!!所以语言层的文件操作函数,本质底层是对系统调用的封装!通过不同标志位的封装来体现w r a+等不同打开类型!

我们在使用文件操作时,一般都要使用语言层的系统调用,来保证代码的可移植性。因为不同系统的系统调用可以会不一样!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

相关文章
|
6月前
|
Ubuntu Linux Anolis
Linux系统禁用swap
本文介绍了在新版本Linux系统(如Ubuntu 20.04+、CentOS Stream、openEuler等)中禁用swap的两种方法。传统通过注释/etc/fstab中swap行的方式已失效,现需使用systemd管理swap.target服务或在/etc/fstab中添加noauto参数实现禁用。方法1通过屏蔽swap.target适用于新版系统,方法2通过修改fstab挂载选项更通用,兼容所有系统。
522 3
Linux系统禁用swap
|
6月前
|
Linux
Linux系统修改网卡名为eth0、eth1
在Linux系统中,可通过修改GRUB配置和创建Udev规则或使用systemd链接文件,将网卡名改为`eth0`、`eth1`等传统命名方式,适用于多种发行版并支持多网卡配置。
1037 3
|
Ubuntu Linux 网络安全
Linux系统初始化脚本
一款支持Rocky、CentOS、Ubuntu、Debian、openEuler等主流Linux发行版的系统初始化Shell脚本,涵盖网络配置、主机名设置、镜像源更换、安全加固等多项功能,适配单/双网卡环境,支持UEFI引导,提供多版本下载与持续更新。
625 3
Linux系统初始化脚本
|
7月前
|
运维 Linux 开发者
Linux系统中使用Python的ping3库进行网络连通性测试
以上步骤展示了如何利用 Python 的 `ping3` 库来检测网络连通性,并且提供了基本错误处理方法以确保程序能够优雅地处理各种意外情形。通过简洁明快、易读易懂、实操性强等特点使得该方法非常适合开发者或系统管理员快速集成至自动化工具链之内进行日常运维任务之需求满足。
457 18
|
6月前
|
安全 Linux Shell
Linux系统提权方式全面总结:从基础到高级攻防技术
本文全面总结Linux系统提权技术,涵盖权限体系、配置错误、漏洞利用、密码攻击等方法,帮助安全研究人员掌握攻防技术,提升系统防护能力。
599 1
|
6月前
|
监控 安全 Linux
Linux系统提权之计划任务(Cron Jobs)提权
在Linux系统中,计划任务(Cron Jobs)常用于定时执行脚本或命令。若配置不当,攻击者可利用其提权至root权限。常见漏洞包括可写的Cron脚本、目录、通配符注入及PATH变量劫持。攻击者通过修改脚本、创建恶意任务或注入命令实现提权。系统管理员应遵循最小权限原则、使用绝对路径、避免通配符、设置安全PATH并定期审计,以防范此类攻击。
1192 1
|
7月前
|
缓存 监控 Linux
Linux系统清理缓存(buff/cache)的有效方法。
总结而言,在大多数情形下你不必担心Linux中buffer与cache占用过多内存在影响到其他程序运行;因为当程序请求更多内存在没有足够可用资源时,Linux会自行调整其占有量。只有当你明确知道当前环境与需求并希望立即回收这部分资源给即将运行重负载任务之前才考虑上述方法去主动干预。
1991 10
|
7月前
|
安全 Linux 数据安全/隐私保护
为Linux系统的普通账户授予sudo访问权限的过程
完成上述步骤后,你提升的用户就能够使用 `sudo`命令来执行管理员级别的操作,而无需切换到root用户。这是一种更加安全和便捷的权限管理方式,因为它能够留下完整的权限使用记录,并以最小权限的方式工作。需要注意的是,随意授予sudo权限可能会使系统暴露在风险之中,尤其是在用户不了解其所执行命令可能带来的后果的情况下。所以在配置sudo权限时,必须谨慎行事。
1155 0
|
7月前
|
Ubuntu Linux 开发者
国产 Linux 发行版再添新成员,CutefishOS 系统简单体验
当然,系统生态构建过程并不简单,不过为了帮助国产操作系统优化生态圈,部分企业也开始用国产操作系统替代 Windows,我们相信肯定会有越来越多的精品软件登录 Linux 平台。
539 0
|
7月前
|
Ubuntu 安全 Linux
Linux系统入门指南:从零开始学习Linux
Shell脚本是一种强大的自动化工具,可以帮助您简化重复的任务或创建复杂的脚本程序。了解Shell脚本的基本语法和常用命令,以及编写和运行Shell脚本的步骤,将使您更高效地处理日常任务。
673 0