Linux 基础IO

Posted DR5200

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 基础IO相关的知识,希望对你有一定的参考价值。

一.C文件IO相关操作

只有文件名但不带路径的话,默认在当前路径下打开文件(以写的方式打开文件若文件不存在会自动创建文件),那到底什么是当前路径呢?

// test目录下 myproc.内容

#include<stdio.h>
int main()
{
        FILE* fp = fopen("test.txt","w");
        if(fp == NULL)
        {
                perror("fopen");
                return 1;
        }
        fclose(fp);
}

可以看出,当前路径和可执行程序所处的路径没有关系,在哪个路径下运行起来可执行程序,该路径就被称为当前路径

为了让大家看的更清楚,稍微修改一下代码,关闭文件后不让进程终止来查看进程的相关信息

// test目录下 myproc.内容

#include<stdio.h>
int main()
{
        FILE* fp = fopen("test.txt","w");
        if(fp == NULL)
        {
                perror("fopen");
                return 1;
        }
        fclose(fp);
        sleep(10000);
}

cwd(current work directory) : 在哪个路径下运行起来可执行程序,进程创建的临时文件都在当前路径下创建
exe : 可执行程序路径

打开文件,读写文件,关闭文件都是进程运行的时候完成的

#include<stdio.h>
int main()
{
        FILE* fp = fopen("test.txt","r");
        if(fp == NULL)
        {
                perror("fopen");
                return 1;
        }
        // 二.
        int count = 5;
        char buffer[64];
        while(count)
        {
        	   // 以\\n为分隔符,一次读取一行内容
                fgets(buffer,sizeof buffer,fp);
                printf("%s",buffer);
                count--;
        }
       
//      一. 
//      int ct = 5;
//      while(ct)
//      {
//              fputs("hello bit\\n",fp);
//              ct--;
//      }
        fclose(fp);
}

在Linux中,一切皆文件,键盘和显示器也可以将其当作文件,那么为什么在C语言当中我们没有打开显示器文件就可以直接使用printf函数进行写入呢?为什么没有打开键盘文件就可以直接使用scanf函数进行读取呢?,原因如下 :

任何进程在运行的时候,默认打开三个输入输出流
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*,(fopen返回值类型,文件指针),FILE* 是C语言的概念,文件描述符是系统级别的概念

#include<stdio.h>
int main()
{
        int count = 5;
        char buffer[64];
        while(count)
        {
                fgets(buffer,sizeof buffer,stdin);
                printf("%s",buffer);
                count--;
        }

        int ct = 5;
        while(ct)
        {
                fputs("hello world\\n",stdout); // strerr
                ct--;
        }
}

由"w" 和 “a” 可以联想到之前讲到的重定向(>)和追加重定向(>>)

test.txt已经存在5行hello world,使用"a"方式打开文件,向文件末尾追加5行hello lyp

#include<stdio.h>
int main()
{
        FILE* fp = fopen("test.txt","a");
        if(fp == NULL)
        {
                perror("fopen");
                return -1;
        }
        int count = 5;
        while(count)
        {
                fputs("hello lyp\\n",fp);
                count--;
        }
        fclose(fp);
}

text.txt中已经存在5行hello world,5行hello lyp,使用"w"方式打开文件,清空原始内容,写入5行hehe

#include<stdio.h>
int main()
{
        FILE* fp = fopen("test.txt","w");
        if(fp == NULL)
        {
                perror("fopen");
                return -1;
        }
        int count = 5;
        while(count)
        {
                fputs("hehe\\n",fp);
                count--;
        }
        fclose(fp);
}

二.系统文件IO

open系统调用接口介绍

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
 O_RDONLY: 只读打开
 O_WRONLY: 只写打开
 O_RDWR : 读,写打开
 这三个常量,必须指定一个且只能指定一个
 O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
 O_APPEND: 追加写
 mode:
 设置文件权限
 返回值:
 成功:新打开的文件描述符
 失败:-1

flags : 系统函数参数传参标志位

传递给flags的这些常量都是宏,通过如下命令可以查看这些宏的定义,可以发现这些宏的二进制序列只有一个1,这些常量进行或运算传递给flags,open函数内部,再通过与运算判断传递了哪些常量

int open(const char *pathname, int flags, mode_t mode)
{
	if(flags & X)
	{}
	if(flags & Y)
	{}
}
open(argu1,X | Y,argu3);
grep -E 'O_CREAT|O_RDONLY|O_WRONLY' /usr/include/ -R

下面我们就来使用一下open

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
	 umask(0);
	 // O_WRONLY|O_CREAT 等价于 fopen 中的"w"
	 int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
     printf("%d\\n",fd);
}

我们想以只写的方式打开test.txt文件,若test.txt文件不存在,会创建一个test.txt文件出来,权限为666
但结果权限为664(默认umask为2),所以我们可以把umask设置为0

#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
        umask(0);

        int fd1 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\\n",fd1);

        int fd2 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\\n",fd2);

        int fd3 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\\n",fd3);

        int fd4 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\\n",fd4);

        int fd5 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\\n",fd5);
}

通过运行结果发现,文件描述符从3开始,这是因为默认打开了标准输入(0),标准输出(1),标准错误(2),所谓的文件描述符是数组下标

write

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

fd : 文件描述符,buf : 写的内容 ,count : 期望写入的字节数
ssize_t : 实际写入的字节数
实际写入的字节数 <= 期望写入的字节数(ssize_t <= count)

// test.txt为空,向test.txt写入5行hello world

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
        umask(0);

        int fd1 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\\n",fd1);

        int count = 5;
        const char* buf = "hello world\\n";
        while(count)
        {
                write(fd1,buf,strlen(buf));
                count--;
        }
}

read

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

fd : 文件描述符,buf : 读取的数据存放的地址 ,count : 期望所读的字节数
ssize_t : 实际所读的字节数
实际所读字节数 <= 期望所读字节数(ssize_t <= count)

// test.txt中有5行hello world,从test.txt中读取字符写入到标准输出中

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
        umask(0);

        int fd1 = open("test.txt",O_RDONLY);
        printf("%d\\n",fd1);

        char c;
        while(1)
        {
                ssize_t s = read(fd1,&c,1);
                if(s <= 0)
                {
                        break;
                }
                write(1,&c,1);
        }
}

C语言提供的IO接口底层都封装了系统调用接口

为什么要进行封装 ?

(1).保证可读性以及调用简单
(2).保证跨平台性 : 我们可以发现C语言可以在任一平台上跑,原因在于C语言在设计时,与系统强相关的不可跨平台的接口都被C语言封装了一遍(我们上面用C语言所写的fopen(),fclose()等函数底层都封装了open,close等系统调用)

open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数

上面的 fopen fclose fread fwrite fgets fputs都是C标准库当中的函数,我们称之为库函数(libc)。
而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口
回忆一下我们讲操作系统概念时,画的一张图

文件描述符 fd

磁盘文件 vs 内存文件

内存文件 : 一个进程可以打开多个文件,在系统中,任何时刻,都可能存在大量的已经打开的文件,对这些文件进行管理依然遵循先描述再组织,struct_file就是描述文件的结构体,以双向链表的方式将其组织起来,对文件的管理就变成了对双链表的增删查改,在内存中有task_struct,struct_file两条双链表,我们想要知道这些被打开的文件哪些属于某一个进程,我们就需要建立进程和文件的对应关系

磁盘文件 : 存在磁盘上的文件并不是只保存了内容,还保存了文件的属性/元信息(文件的名字,创建日期,大小)

文件 = 内容 + 属性

我们将文件打开时,是有两份的,一份在硬盘上,一份在内存里(内存文件更多的是文件的属性信息),当我们想要读写数据时,再延后式的慢慢加载数据到内存(这里会有缓冲区的概念)

// 人和操作系统唯一的交互方式就是进程,而进程和操作系统的交互方式是系统调用接口

open系统调用打开文件的过程

(1). 进程打开一个文件,内存中创建struct_file结构体,存储打开文件的属性信息
(2). 操作系统为该文件分配文件描述符(fd_array数组中未被分配的且最小的下标)
(3). 将struct_file结构体的地址填到fd_array数组对应的位置,返回该文件的文件描述符

由此可知read,write等系统调用为什么要传入fd ?,因为拿到fd可以找到文件对应的struct_file结构体,得到文件的相关信息

重定向

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
        umask(0);
        int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
        if(fd < 0)
        {
                return 1;
        }

        write(1,"hehe\\n",5);
        write(1,"hehe\\n",5);
        write(1,"hehe\\n",5);
        write(1,"hehe\\n",5);
        write(1,"hehe\\n",5);

        close(fd);
}
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
        umask(0);
        close(1);
        int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
        if(fd < 0)
        {
                return 1;
        }

        write(fd,"hehe\\n",5);
        write(fd,"hehe\\n",5);
        write(fd,"hehe\\n",5);
        write(fd,"hehe\\n",5);
        write(fd,"hehe\\n",5);

        close(fd);
}

对比这两段代码,我们会发现关闭显示器文件后,fd的值为1,即test.txt文件的文件描述符为1,达到的效果为原本要写入到显示器当中的内容写入到了test.txt中,这种现象叫做输出重定向(>)

重定向的本质 : 修改文件描述符对应的struct_file*的指向

// 输出重定向
cat > test.txt

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
        umask(0);
        close(1);
        int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
        if(fd < 0)
        {
                return 1;
        }
	
        printf("hello world\\n");
        fprintf(stdout,"hello fprintf\\n");
        fputs("hello fputs %d %d %f %c\\n",stdout);
		// 需要刷新缓冲区
        fflush(stdout);

        close(fd);
}

解释这段代码前先进行一下知识的铺垫

实际上,C语言的 fopen 函数打开文件主要做了以下三件事情
(1). 创建FILE结构体
(2). 底层调用open函数打开文件,返回fd,将fd填充到FILE结构体当中的 _fileno(封装的文件描述符)
(3). 返回FILE结构体的地址(FILE*)

所以我们C语言中平常所使用的fread,fwrite,fgets,fputs中拿到我们传递的参数stream(FILE* stream),根据stream指针找到struct FILE结构体,struct FILE结构体中封装了文件的fd,拿到fd后,去task_struct->files_struct->fd_array数组中得到文件对应的struct_file结构体,由此得到文件信息进行读取或写入

之前也提到过,任何一个进程,默认会打开3个文件 : 标准输入(键盘文件),标准输出(显示器文件),标准错误(显示器文件),在C语言中创建对应的FILE结构体,调用open函数打开文件,用返回值fd填充 _fileno,返回FILE结构体的地址给stdin,stdout,stderr(FILE*指针)

再来看这段代码,我们首先关闭了1号文件描述符,open打开文件后,1号文件描述符被分配给了test.txt文件,代码中 printf/fprintf/fputs 参数stream都为stdout,stdout->struct FILE结构体->fd->struct_file结构体,此时struct_file结构体对应test.txt文件,因此内容写入到了test.txt文件当中

// 输入重定向
cat < test.txt

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
        umask(0);
        close(0);
        int fd = open("test.txt",O_RDONLY);
        if(fd < 0)
        {
                perror("open");
                return 1;
        }
        char buffer[50];
        fgets(buffer,50,stdin);
        printf("%s\\n",buffer);
		
		close(fd);
}

// 追加重定向

cat >> test.txt

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
        umask(0);
        close(1);
        int fd = open("test.txt",O_WRONLY|O_APPEND); // 等价于C语言 fopen 中的"a"选项
        if(fd < 0)
        {
                perror("open");
                return 1;
        }

        printf("hello world\\n");
        fprintf(stdout,"hello fprintf\\n");
        fputs("hello fputs %d %d %f %c\\n",stdoutLinux基础IO篇

20155201 李卓雯 《网络对抗技术》实验一 逆向及Bof基础

逆向及Bof基础实践

20155307刘浩《网络对抗》逆向及Bof基础

20155311高梓云《网络对抗》逆向及Bof基础

20145301赵嘉鑫《网络对抗》逆向及Bof基础