Linux系统编程-基础IO(文件操作)

文章目录

  • 一. C语言库函数读写文件
    • 1.1 fgets
    • 1.2 fputs
  • 二. 文件操作的系统调用接口
    • 2.1 open和close
    • 2.2 write
    • 2.3 read
  • 三. 文件描述符
    • 3.1 概念
    • 3.2 文件描述符的分配规则
  • 四. 重定向
    • 4. 1 概念
    • 4.2 输出重定向和追加重定向
    • 4.3 输入重定向
    • 4.4 dup2接口
    • 4.5 支持重定向的minishell
  • 五. 文件缓冲区
    • 5.1 概念
    • 5.2 验证文件缓冲区刷新策略
    • 5.2 系统接口和库函数读写的缓冲区
  • 六. Linux文件系统
    • 6.1 简述
    • 6.2 软硬链接
      • 6.2.1 简介
      • 6.2.2 创建和解除软硬链接关系
  • 七. 文件的三个时间
  • 八. 动静态库
    • 8.1 概念
    • 8.2 制作和使用静态库
      • 8.2.1 相关源文件
      • 8.2.2 静态库
        • 8.2.2.1 制作
        • 8.2.2.2 使用
      • 8.2.3 动态库
        • 8.2.3.1 制作
        • 8.2.3.2 使用
  • 九. 相关命令
    • 9.1 file
    • 9.2 ldd
    • 9.3 stat

一. C语言库函数读写文件

1.1 fgets

// 读取一个字符串
// char* fgets(char* s, int size, FILE* stream);
// size为读几个字符
void test_fgets()
{
    FILE* fd = fopen("file1.txt", "r");

    char s[100] = "";
    fgets(s, 5, fd);  // 实际读取的是size-1个字符,还有一个放'\0'
    printf("%s\n", s);
    
    fclose(fd);
    fd = NULL;
}

1.2 fputs

// 写入一个字符串
// int fputs(const char* s, FILE* stream);
void test_fputs()
{
    FILE* fd = fopen("file1.txt", "w");

    char s[100] = "hello";
    fputs(s, fd);
    
    fclose(fd);
    fd = NULL;
}

二. 文件操作的系统调用接口

2.1 open和close

#include
#include
#include
#include  // for open
#include   // for close
// 用系统接口打开、关闭一个文件
int main()
{
    // int open(const char* pathname, int flags);
    // int open(const char* pathname, int flags, mode_t mode);
    // pathname:文件的路径和名字
    // flags:打开文件的方式,是用比特位去映射这些方式()
    // O_RDONLY --> 只读    O_WRONLY --> 只写     O_RDWR --> 读写(这三个必须指定一个且只能指定一个)
    // O_CREAT --> 如果文件不存在就自动创建 (需要配合有mode的open接口,来指明新文件的权限)
    // O_APPEND --> 追加
    // O_TRUNC --> 如果文件已经存在且是正常可以写入的,则将文件的数据清空
    // mode 设置新建文件的权限,一般设置0644
    //int fd = open("file.txt", O_RDWR | O_CREAT);  
    int fd = open("file.txt", O_RDWR | O_CREAT, 0644);
    close(fd);
    return 0;
}

2.2 write

int main()
{
    int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
    if (fd < 0)
    {
        perror("open");
        return -1;
    }
    // ssize_t write(int fd, const void* buf, size_t count);
    // ssize_t --> int 返回的是写入的实际字节数
    // count为一共写入的字节数
    const char* str = "hello world\n";
    ssize_t write_ret = write(fd, str, strlen(str) * sizeof(char));
    close(fd);
    return 0;
}

2.3 read

int main()
{
    int fd = open("file.txt", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return -1;
    }
    char str[128];
    // ssize_t read(int fd, void* buf, size_t count);
    ssize_t read_ret = read(fd, str, sizeof(str));
    if (read_ret > 0)
    {
        printf("%s", str);
    }
    else
    {
        perror("read");
        return -1;
    }
    close(fd);
    return 0;
}

三. 文件描述符

3.1 概念

C语言库函数文件操作是通过FILE*指向的对象实现操作的,而系统的接口是通过int类型的fd实现的,这个fd就是我们的文件描述符。如图所示,创建一个进程时,进程的相关信息存放在一个task_struct的结构体,task_struct里面有一个files的指针变量指向的是一个file_struct结构体,这个结构体里面又有一个名为fd_arrrydfile*类型的指针数组,这个数组的存放的就是打开的各个文件的地址,我们的文件描述符的数字就是这个指针数组的下标。Linux系统默认会给每一个进程打开三个文件,分别是stdin(标准输入)、stdout(标准输出)和stderr(标准错误),他们的文件描述符分别为0,1,2,所以正常情况下,打开的文件描述符是从3开始的。

Linux系统编程-基础IO(文件操作)_第1张图片

// 下面代码输出的分别是3 4 5 6,实际上文件描述符是从0开始的,创建进程时系统默认会打开stdin stdout stderr,0 1 2被他们占去了。
int main(
{
    int fd1 = open("file1.txt", O_WRONLY | O_CREAT, 0644);
    int fd2 = open("file2.txt", O_WRONLY | O_CREAT, 0644);
    int fd3 = open("file3.txt", O_WRONLY | O_CREAT, 0644);
    int fd4 = open("file4.txt", O_WRONLY | O_CREAT, 0644);
    printf("fd1 -- > %d\n", fd1);  
    printf("fd2 -- > %d\n", fd2);  
    printf("fd3 -- > %d\n", fd3);
    printf("fd4 -- > %d\n", fd4);
    return 0;
}

3.2 文件描述符的分配规则

在fd_array中找一个没有被用到的最小的数组下标,作为新建文件的描述符

int main()
{
    // 我们关闭了1号文件, 然后打开file.txt文件,发现该文件的描述符变成了1,而不是3
    close(1);
    int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
    if (fd == -1)
    {
        perror("open");
        return 0;
    }
    else
    {
        printf("fd --> %d\n", fd);
    }
    return 0;
}

四. 重定向

4. 1 概念

输出重定向就是本来应该写入stdout(标准输出)的内容,被写到了其他文件,追加重定向和输入重定向也是同理。

Linux系统编程-基础IO(文件操作)_第2张图片

4.2 输出重定向和追加重定向

// 输出重定向和追加重定向
int main()
{
    close(1);  // 关闭了标准输出文件
    int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);  // 这里新建的file.txt的文件描述符就会分配成1
    // int fd = open("file.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);  // 这里或上O_APPEND就是追加重定向了
    if (fd < 0)
    {
        perror("open");
        return -1;
    }
    const char* msg = "hello world";
    
    // printf就是往标准输出写入信息,默认情况下标准输出stdout的文件描述符是1,在用printf等接口向标准输出写入文件时,它只认文件描述符1号。因为我们打开的file.txt的文件描述符被分配为1,所以我们这里printf打印的东西会被写入到file.txt
    printf("fd --> %d\n", fd);  
    printf("%s\n", msg);
    fflush(stdout);  // 后面会解释为什么会执行这句
    close(fd);
    return 0;
}

4.3 输入重定向

// 输入重定向
int main()
{
    close(0);  // 关闭了标准输入文件
    int fd = open("file.txt", O_RDONLY | O_CREAT, 0644);  // 这里新建的file.txt的文件描述符就会分配成0
    if (fd < 0)
    {
        perror("open");
        return -1;
    }
   	char msg[128];
    
    // 这里file.txt的文件描述符为0,本来应该向标准输入(键盘)读取信息,现在变成了向file.txt读取
    scanf("%s", msg); 
    printf("%s\n", msg);
    close(fd);
    return 0;
}

4.4 dup2接口

dup2是一个系统调用接口,作用是将一个文件描述符oldfd下标位置的数据拷贝到另外一个文件描述符newfd下标的位置注意: 拷贝之后原来oldfd位置的数据还在。

Linux系统编程-基础IO(文件操作)_第3张图片

// int dup2 (int oldfd, int newfd)  // oldfd --> newfd
// 成功会返回newfd,不成功返回-1
int main()
{
    int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
    dup2(fd, 1);  // 执行之后,1下标存的是file.txt文件的地址
    
    // 下面两个printf输出的数据就会被重定向到file.txt
    printf("I should be written on the monitor!\n");
    printf("The file.txt fd is %d,and the msg should be written on the monitor\n", fd);
    write(fd, "I should be written in file.txt\n", 32);
    fflush(stdout);  // 后面会解释为什么会执行这句
    close(1);
    return 0;
}

4.5 支持重定向的minishell

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define CMD_NUM 16
#define NUM 64

// 获取从终端输入的字符串
void get_command(char* command)
{
    command[0] = '\0';  // 
    printf("[li@ThinkPad:~#] ");
    fflush(stdout);
    fgets(command, NUM, stdin);
    command[strlen(command) - 1] = '\0';
}


// 检测和处理输出重定向
int do_redirect(char* command)
{
    char* copy = command;  // 拷贝初始地址,后面运行完毕需要将command指向一开始的地址
    int flags = -1;  // 初始值为-1,有一个'>'就flags++,通过flags的取值判断是否有输出重定向或者追加重定向
    while (*command != '\0')
    {
        if (*command == '>')  // 检测第一个'>'
        {
            *command = '\0';  // 检测完之后将这个'>'的这个位置置成'\0',方便后期do_parse的时候能够正常解析
            command++;
            flags++;
        
            if (*command == '>')
            {
                *command = '\0';
                command++;
                flags++;
            }
            
            // 走完 > 之后的空格
            // int isspace(int c);  --> 检查c是不是空字符,不是就返回0,是返回非0
            while (isspace(*command))
            {
                command++;
            }
            
            // 到了这里应该就是重定向的文件名了,下面就是走完文件名这个字符串 
            char* file_name = command;
            while (!isspace(*command) && *command != '\0')
            {
                command++;
            }
            *command = '\0';  // 这里置'0',表示文件名字符串的结束标志
            if (flags == 0)
            {
                int fd = open(file_name, O_WRONLY | O_CREAT | O_TRUNC, 0644);  // O_TRUNC 如果文件已经存在且是正常可以写如的,则将文件的数据清空
                dup2(1, 2);  // 将标准输出的地址保存起来,前提是你不用stderr的情况下
                dup2(fd, 1); 
                return flags;
            }
            else if (flags == 1)
            {
                int fd = open(file_name, O_APPEND | O_CREAT | O_WRONLY, 0644);
                dup2(1, 2);
                dup2(fd, 1);
                return flags;
            }
        }
        command++;
    }
    command = copy;  
}

void do_parse(char* command, char** argv)
{
    int argc = 0;
    char* copy = command;
    while (*command != '\0')
    {
        // 检测一个命令字符的开头
        if(!isspace(*command))
        {
            argv[argc] = command;
            argc++;
            
            // 检测一个命令字符的结尾
            while (!isspace(*command) && *command != '\0')
            {
                command++;
            }
            *command = '\0';
            command++;
        }
        else
        {
            // 检测空白字符的结尾
            while (isspace(*command))
            {
                command++;
            }
        }
    }
    argv[argc] = NULL;
}

int do_exec(char** argv)
{
    int pid = fork();
    if (pid < 0)
    {
        perror("fork");
        return 1;
    }
    else if (pid == 0)
    {
        if (argv[0] == NULL)
        {
            return -1;
        }
        execvp(argv[0], argv);
    }
    else
    {
        waitpid(pid, NULL, 0);
    }
}
int main()
{
    char command[NUM];
    char* argv[CMD_NUM];
    while (1)
    {

        get_command(command);
        int flags = do_redirect(command);
        do_parse(command, argv);
        //int i = 0;
        //for (i = 0; argv[i]; i++)
        //{
        //    printf("%s ",argv[i]);
        //}
        do_exec(argv);
        if (flags != -1)
        {
            close(1);
            dup2(2, 1);
        }
    }
    return 0;
}

五. 文件缓冲区

5.1 概念

如下图所示,在用户层面有一个文件缓冲区,这个缓冲区是系统为在内存中为进程的每一个打开的文件开辟的一块区域,不同文件有着不同的刷新策略,刷新就是将用户层面缓冲区的数据传输到OS层面的缓冲区,然后再刷新到外设上面。

Linux系统编程-基础IO(文件操作)_第4张图片

5.2 验证文件缓冲区刷新策略

// 下面的代码执行后会直接直接在显示器打印hello wrold
void test1()
{
    printf("hello world\n");
}

// 下面代码会发生重定向,hello world会被写如file.txt文件
void test2()
{
    close(1);
    int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
    printf("hello world\n");
}

// 下面的代码会发生重定向,但是hello wrold不会被写如file.txt
// 因为发生重定向之后,hello world写入的目标文件发生了变化(由显示器变成的磁盘)
// 伴随着刷新的策略也变了,由行刷新(遇到\n就刷新) --> 缓冲满了才刷新
// test2为什么能写入成功,而test3不能?
// 因为test2没有关闭文件,在程序结束之后会自动刷新缓冲区。而test3在程序结束之前关闭了文件,从而缓冲区没有被及时刷新到
void test3()
{
    close(1);
    int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
    printf("hello world\n");
    close(fd);
}

// test4在test3的基础上,关闭文件之前调用fflush强制刷新缓冲区,最后hello world会被写到file.txt
void test4()
{
    close(1);
    int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
    printf("hello world\n");
    fflush(stdout);
    close(fd);
}

5.2 系统接口和库函数读写的缓冲区

下面两个代码,第一个正常运行打印,第二个除了系统调用接口,其他两个库函数接口都往文件里面写了两遍。第二个代码打印两遍的原因是,printf和fprintf的数据会被写到file.txt的缓冲区,file.txt文件缓冲区刷新策略是缓冲区满了才刷新,子进程创建的时候缓冲区还没有刷新,进程结束时,会刷新文件缓冲区。无论父子哪个程序先结束都会触发写时拷贝,所以最后导致的结果就是父子进程结束之后都被刷新缓冲区,最后写入file.txt是也会被写如两次。而write是系统接口,write写的数据会直接写在外设,没有缓冲区,在创建子进程之前已经写完了。

Linux系统编程-基础IO(文件操作)_第5张图片

六. Linux文件系统

6.1 简述

下图是Linux系统ext系列文件系统的大致结构,从下图我们可以看到,一块硬盘可以被分成若干个分区,一个分区里面有一个BootBlock和若干个Block group,一个Block group里面又分成了几个部分,下面是对这几部分的说明。

Super Block: 超级快,存放文件系统本身的结构信息,主要有block和inode总量,未使用的block和inode的数量,一个block和inode的大小等。

Group Descriptor Table: 块组描述符,描述块组属性信息。

Block BItmap: 块位图,记录数据块是否被占用。

inode Bitmap: 记录inode是否被占用

inode Table: 这个表的一个元素对应着一个文件,里面存放一个文件的属性。比如文件对应的inode,和引用计数ref,和文件数据块的数组blocks[]等。inode是文件的唯一标识,一个文件只有一个inode,但是文件名可以有很多个,文件名和inode是多对一的映射关系的,ref就是记录这个文件映射了几个名字,blocks[]装的是这个文件对应的数据块信息。

Data Block: 数据块,存放文件的内容。

Linux系统编程-基础IO(文件操作)_第6张图片

6.2 软硬链接

6.2.1 简介

软连接: 相当于Window下的快捷方式,是一个独立的文件,有自己的inode和数据块,数据块保存的是指向文件的路径和文件名。

硬链接: 不是一个独立的文件,因为没有自己独立的inode。创建硬链接的本质就是在特定目录下,填写一对文件名和inode的映射关系。

6.2.2 创建和解除软硬链接关系

# 比如说当前目录有一个file.txt文件
# 创建软链接
ln -s file.txt file1  # 这里的file1就是file.txt的一个软连接

# 创建硬链接
ln file.txt file12  # file2是file.txt的硬件链接

# 解除软硬链接关系
ln file  # file是软硬链接都是这样的

七. 文件的三个时间

Access: 最后访问的时间(在较新的Linux内核中,Access时间不会立即刷新,而是有一定的刷新间隔,OS才会自动更新)

Modify: 最后修改文件内容的时间

Change: 最后修改文件属性的时间,修改内容也可能伴随着属性的修改

应用场景: Makefile在gcc时就是需要源文件比可执行文件的Modifi时间更新,才会重新编译源文件。

八. 动静态库

8.1 概念

静态库: 库文件是以.a作为后缀的,程序在编译链接的时候把库链接到可执行文件中,程序运行时不需要静态库。

动态库: 库文件是以.so作为后缀的,一个动态链接的可执行文件仅仅包含它用到的函数入口地址的一个表,在运行时才去链接动态库,链接过程称为动态链接(dynamic linking)。动态库可以在多个程序间共享,可执行文件更小。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

库文件的命名: libxxx.so 或者libxxx.a,xxx就是库名称。

8.2 制作和使用静态库

8.2.1 相关源文件

// sub.h文件
#pragma once
extern int sub(int a, int b);

// add.h文件
#pragma once
extern int add(int a, int b);

// add.c文件
#include "add.h"
int add(int a, int b)
{
    return a + b;
}

// sub.c文件
#include "add.h"
int sub(int a, int b)
{
    return a -b;
}

// code.c文件
#include 
#include "add.h"
#include "sub.h"

int main()
{
    int a = 5;
    int b = 10;
    printf("a + b = %d\n", add(a, b));
    printf("a - b = %d\n", sub(a, b));
    return 0;
}

8.2.2 静态库

8.2.2.1 制作
# Makefile文件
libmymath.a:sub.o add.o
	# ar是归档工具,rc表示replace和create
	ar -rc $@ $^
%.o:%.c
	gcc -c $<
.PHONY:clean
clean:
	rm -rf *.o libmymath.a
.PHONY:install
install:
	cp -rf libmymath.a ../mylib
	cp -rf *.h ../myinclude
8.2.2.2 使用
# Makefile文件
mycode:code.c
	# -L指明静态库的路径,-l指明库名称(库文件名去除lib前缀和.a或者.so),后面跟库名称不用空格隔开
	#  还有-I可以指明头文件(这里.h文件在当前目录可以不用)
	#  如果动静态库同时存在,默认会链接动态库,想要链接静态库需要在加上 -static
	gcc -o $@ $^ -I ../myinclude -L ../mylib -lmymath -static
.PHONY:clean
clean:
	rm -rf mycode

8.2.3 动态库

8.2.3.1 制作
# Makefile文件
# 形成一个动态链接的共享库
libmymath.so:add.o sub.o
	gcc -shared -o $@ $^
# -fPIC产生与位置无关码(position independent code)
%.o:%.c
	gcc -fPIC -c $<
.PHONY:clean
clean:
	rm -rf *.o libmymath.so
.PHONY:install
install:
	cp -rf libmymath.so ../mylib
	cp -rf *.h ../myinclude
8.2.3.2 使用
# Makefile文件
mycode:code.c
	gcc -o $@ $^ -I ../myinclude -L ../mylib -lmymath
.PHONY:clean
clean:
	rm -rf mycode

Makefile生成的mycode还不能直接运行,因为动态链接在编译的时候知道库在哪里,在运行的时候也需要自动库在哪里,所以我们需要指明库的路径。

解决的方法有三种:

  1. 将动态库拷贝到系统共享库的路径下(这种方法不推荐,会污染其他库)
  2. 在/etc/ld.so.conf.d/路径下创建一个xxx.conf文件,然后将库的绝对路径写进这个文件,最后用ldconfig更新一下。
  3. 更改LD_LIBRARY_PATH,export LD_LIBRARY_PATH= (=后面填写库的绝对路径,这种方法只在本次打开有效)

九. 相关命令

9.1 file

# 识别文件的类型
file filename

9.2 ldd

# 列出动态库依赖关系(list dynamic dependencies)
ldd myexe  # myexe是一个动态可执行文件

9.3 stat

# 显示文件或文件系统的详细信息
stat filename

你可能感兴趣的:(Linux系统编程,linux,c语言,读写文件,基础IO,操作系统)