文件 fd

🏷️ 预备工作

❓ 当我们在系统中创建一个空文件时,这个文件要不要占据磁盘的空间(注意是空文件哟)

答案:当然是要占据磁盘的空间的,文件不仅只包括内容,还有它的属性呀,就是创建时间,用户是谁,修改时间之类的,所以即便这个文件是一个空文件,但它也有属性呀,所以也要占据空间

文件 = 内容 + 属性

📌 所有对文件的操作,无非就是两种:

  1. 对内容的操作
  2. 对属性的操作

📌 文件的内容是数据,文件的属性也是数据,我们存储文件必须把文件的内容和属性都存储

📌 我们如果要访问一个文件,我们要先打开它,问题来了 ❓:

  1. 我们指的是谁?
  2. 文件打开前是什么意思?
  3. 文件打开之后是什么意思?
    1. 这里的我们指的是进程,不是你自己哈。比如你在你写的程序之中写了 fopen 这个函数来打开一个文件,但是只有这个程序编译形成可执行程序之后才可以去执行这个fopen的操作,一个被加载到内存的可执行程序叫什么呀?—— 不就是进程吗?
    1. 文件打开之前,就是一个普通的磁盘文件,是在磁盘上面的
    1. 打开后这个文件,其实上是将这个文件加载到内存

📌 一个进程可以打开多个文件吗? 多个进程可以打开多个文件吗?

  • 一个进程可以打开多个文件,多个进程也可以打开多个文件

加载到内存中被打开的文件,可能存在多个

文件本来是在磁盘中的,将文件从磁盘中加载到内存里一定要涉及到访问磁盘设备,但是磁盘是外设,把磁盘里的数据加载到内存中的这个工作只能是由操作系统来做

📌 操作系统在运行的时候可能会打开很多个文件,那么操作系统是如果来管理这些文件的呢?

由我们之前学习的知识可知,如果涉及到管理,一般的步骤是:先描述在组织

✏️先描述:

一个文件要被打开,一定要在内核中形成被打开的文件对象
类似这样:

struct xxxx
{
	// 文件的属性
	xxxxx
	struct xxxx *next;
};

在这里插入图片描述

📌 文件按照是否被打开分为:被打开的文件和没有被打开的文件

被打开的文件是存放在内存中的,没有被打开的文件是存放在磁盘中的

📌 本次学习的目的是:研究进程和打开文件之间的关系

🏷️ 复习一下常见的 C 的文件接口

📌 材料准备:

我们需要:test.c , Makefile

Makefile:

myfile:myfile.c

	gcc -o $@ $^ 

.PHONY:clean

clean:

	rm -f myfile

test.c

#include <stdio.h>

int main()
{
    // "w": 按照写的方式来打开文件,如果文件不存在就创建
    FILE *fd = fopen("log.txt", "w");
    if (fd == NULL)
    {
        perror("fopen");
        return 1;
    }
	fclose(fd);  // 关闭文件
    return 0;
}
  1. FILE *fd = fopen("log.txt", "w");
    这行代码调用了C标准库函数fopen,尝试以写入模式(“w”)打开名为log.txt的文件。如果文件不存在,fopen会尝试创建它。fopen函数返回一个指向FILE对象的指针,这个对象包含了所有与文件操作相关的信息。这个指针被存储在变量fd中,fd是一个指向FILE类型的指针。

  2. if (fd == NULL) {
    这行代码检查fopen是否成功打开了文件。如果fopen不能打开文件(可能是因为文件不存在且没有权限创建,或者磁盘空间已满等原因),它会返回NULL。因此,这个if语句检查fd是否为NULL

  3. perror("fopen");
    如果fdNULL,这意味着fopen函数失败了。在这种情况下,perror函数被调用来打印一条错误消息。perror函数的第一个参数是一个字符串,后面跟着一个冒号和一个空格。这个字符串是用户提供的,用来标识错误消息的来源。在这个例子中,字符串是"fopen"perror会将这个字符串与errno全局变量中存储的错误代码相结合,errno是在尝试打开文件时由fopen设置的。然后,perror将打印出相应的错误消息到标准错误输出(通常是你的控制台或终端)。

  4. return 1;
    如果文件打开失败,函数返回1。这是一种约定,表示函数因为遇到错误而提前终止。返回值1通常表示错误或异常状态,而返回0通常表示成功。

运行上面👆🏻的代码,./mytest ,可以发现我们的当前工作目录中已经出现了log.txt 这个文件。

在这里插入图片描述

向这个文件中写入内容

    const char *msg = "hello linux file\n";
    int cnt = 10; 
    while(cnt)
    {
        fputs(msg, fd);
        cnt--;
    }

fputs 是 C 语言标准库中的一个函数,用于将一个字符串写入到文件中。这个函数声明在 <stdio.h> 头文件中,其原型如下:

int fputs(const char *str, FILE *stream);

整体代码

// myfile.c

#include <stdio.h>

int main()
{
    // "w": 按照写的方式来打开文件,如果文件不存在就创建
    FILE *fd = fopen("log.txt", "w");
    if (fd == NULL)
    {
        perror("fopen");
        return 1;
    }

        // 向这个文件中写入内容
        const char *msg = "hello linux file\n";
        int cnt = 10;
        while(cnt)
        {
            fputs(msg, fd);
            cnt--;
        }
    
    fclose(fd);
    return 0;
}

运行上面的代码之后我们可以放心 log.txt 中已经被写入了内容。

注意:以 “w” 的方式打开这个文件,会覆盖掉这个文件之前的内容。只要你以"w"的方式打开了,无论你做没做修改,之前的文件的内容都会被清空。
我们如果要清空一个文件的内容,可以使用命令:> 文件名 , 比如:> log.txt> 是重定向,这里由于没有任何的前置操作,所以会被我们的 shell 解释成: 首先我们要重定向,就要先把这个文件给打开,但是由于没有前置的命名,所以最后又关闭这个文件,但是默认是以写的方式来打开这个文件的,所以这个文件的内容被清空了。

**除了 “w”的方式来写入,我们还可以使用“a”方式来写入,代码如下:

#include <stdio.h>

int main()
{
    // "w": 按照写的方式来打开文件,如果文件不存在就创建
    FILE *fd = fopen("log.txt", "a"); /// 这里 a 方式
    if (fd == NULL)
    {
        perror("fopen");
        return 1;
    }

        // 向这个文件中写入内容
    const char *msg = "message text";
    fputs(msg,fd);
    fclose(fd);
    return 0;
}

a 方式也是写入,和 w 方式的区别是:a 方式不会删除之前的内容,它是从文件的结尾处开始写入,即:追加,不清空

我们上面说的 > :是输出重定向,使用 > log.txt 会删除这个log.txt 之前的内容,我们也可以使用 >> :这个是追加重定向,使用>> log.txtt 不会删除log.txt 之前的内容而是追加

🏷️ 认识系统接口,操作文件

一个进程是通过操作系统来打开文件的,所以操作系统一定会提供相应的系统调用接口,我们学习的 c语言的文件相关的函数fopen,fclose 之类的。底层一定是封装了系统的调用接口的

📌 认识系统调用接口:open

在Linux系统中,open 系统调用用于打开或创建一个文件,并返回一个文件描述符,该文件描述符用于后续的文件操作。以下是对 open 系统调用的详细解释:

头文件

要使用 open 系统调用,你需要包含以下头文件:

#include <fcntl.h>

fcntl.h 头文件包含了文件控制选项,包括 open 系统调用的定义。

函数原型

open 系统调用的函数原型如下:

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

pathname : 是我们要打开的文件的名字
flags : 是标志位

在C语言中,open 函数是用来打开文件的系统调用,它定义在 <fcntl.h> 头文件中。open 函数的两个参数 pathnameflags 决定了如何打开文件。

pathname 参数是一个指向字符数组的指针,它包含了要打开的文件的路径。这个路径可以是相对路径也可以是绝对路径。

flags 参数是一个或多个标志位的组合,这些标志位定义了文件打开的方式。在Linux系统中,常用的标志位包括:

  • O_RDONLY:以只读方式打开文件。如果文件不存在,打开操作将失败。
  • O_WRONLY:以只写方式打开文件。如果文件不存在,会创建一个新文件。
  • O_RDWR:以读写方式打开文件。如果文件不存在,打开操作将失败。
  • O_CREAT:如果文件不存在,则创建新文件。通常与 O_WRONLYO_RDWR 结合使用。
  • O_TRUNC:如果文件已存在且成功打开,则将其长度截断为0。
  • O_APPEND:设置文件的读写位置在文件末尾。通常用于写操作。
  • O_EXCL:与 O_CREAT 一起使用,如果文件已存在,则 open 调用失败。
  • O_NONBLOCK:以非阻塞方式打开文件。对于某些类型的文件(如终端设备),这可以使 readwrite 调用立即返回而不是阻塞。
  • O_SYNC:打开文件进行同步I/O操作。每次 write 调用都会等待数据实际写入磁盘。

这些标志位可以组合使用,以提供不同的文件打开选项。例如,如果你想以读写方式打开一个文件,并且如果文件不存在则创建它,你可以这样设置 flags

int flags = O_RDWR | O_CREAT;

在实际使用中,flags 参数的值通常是这些标志位的位或(bitwise OR)操作的结果。

❓ 如何理解这个标记位 flags

我们可以做一个简单的实验:
我们创建一个 test.c 文件
文件内容如下:

#include <stdio.h>

  

#define Print1 1  // 0001 ------ 这是 1 的二进制表达形式

#define Print2 (1<<1) // 0010  左移 1 位

#define Print3 (1<<2) // 0010  左移 2 位

#define Print4 (1<<3) // 1000  左移 3 位

  

void Print(int flags) // 这个函数的作用是用来打印我们想打印的数

{

  if (flags&Print1) printf("hello 1\n");

  if (flags&Print2) printf("hello 2\n");

  if (flags&Print3) printf("hello 3\n");

  if (flags&Print4) printf("hello 4\n");

}

  

int main()

{

  Print(Print1);  // 我们想打印 1  

  Print(Print1|Print2); // 我们想打印 1,2,观察我们传入的参数,是 Print1 和 Print2 相或的结果作为参数:flags ,flags 被传入我们的函数 Print 之后在通过相应的 & 运算来得出我们想要的结果,我们这里就是来类比体会一下 open 函数中标志位的用法,以下同理

  Print(Print1|Print2|Print3); // 我们想打印 1,2,3,注意我们这里参数的写法,这样写法就类似 open 函数里面的标志位 flags 的用法

  Print(Print3|Print4); // 我们想打印 3, 4

  Print(Print4);  // 我们想打印 4。

  return 0;

}

运行结果:

在这里插入图片描述

==即:我们可以采用宏的方式 ,来向一个函树,可以批量化的传递多种标志位,任意标志位进行组合 ==

在Linux中,open 系统调用还有一个额外的参数 mode,用于设置新创建文件的权限:

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

返回值

  • 如果 open 系统调用成功,它将返回一个非负的文件描述符(它返回的那个 int 类型的数,我们称之为文件描述符)。
  • 如果调用失败,它将返回 -1,并设置全局变量 errno 以指示错误原因。

参数

  1. pathname:一个指向文件名的指针。这个文件名可以是相对路径或绝对路径。

  2. flags:一个标志位,用于指定文件打开的方式。常见的标志位包括:

    • O_RDONLY:以只读方式打开文件。(read only 缩写)
    • O_WRONLY:以只写方式打开文件。(write only)
    • O_RDWR:以读写方式打开文件。(read write)
    • O_CREAT:如果文件不存在,则创建一个新文件。( creat )
    • O_TRUNC:如果文件已存在且成功打开,则将文件长度截断为0。
    • O_APPEND:如果文件已存在,写入操作会在文件末尾追加数据。(appear end)
    • O_EXCL:与 O_CREAT 一起使用,如果文件已存在,则 open 调用失败。
  3. mode(可选):当创建新文件时,mode 参数指定了文件的权限模式。如果不需要创建新文件,这个参数通常被设置为0。mode_t 是一个数据类型,用于表示文件的权限模式。

用法

以下是使用 open 系统调用打开文件的示例:

// myfile.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // close 要用


int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT); // O_WRONLY:只写入,O_CREAT:文件不存在就创建
    if (fd < 0) // 如果失败的话,会返回-1
    {
        perror("open");
        return 1;
    }
    close(fd);
    return 0;
}

我们运行上面的程序,发现结果是这样的:
在这里插入图片描述

当我们在新建这个文件的时候,我们并没有给它指明我们要以什么样的权限来创建这个文件,所以这里的权限 是乱码。所以我们要修改一下我们的代码,给open 函数加上第 3 个参数:mode_t mode 代表的就是权限。

// myfile.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // close 要用


int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); // O_WRONLY:只写入,O_CREAT:文件不存在就创建
    if (fd < 0) // 如果失败的话,会返回-1
    {
        perror("open");
        return 1;
    }
    close(fd);
    return 0;
}

在这里插入图片描述

在这里插入图片描述
(1.48)

📌 认识系统调用接口: write (对文件进行操作)

在 Linux 系统中,write 系统调用是一种基本的系统调用接口,用于将数据从用户空间写入到文件描述符指向的文件中。以下是对 write 系统调用的详细解释:

函数原型

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

在这里插入图片描述

参数

  • fd:文件描述符(file descriptor),这是一个非负整数,用于标识一个已经打开的文件或者套接字。
  • buf:指向要写入数据的缓冲区的指针。这个缓冲区中的数据将被写入到文件描述符 fd 指向的文件中。
  • count:要写入的字节数。

返回值

  • 成功时,write 返回写入的字节数。这个值应该与 count 相等,除非遇到错误或者文件末尾(EOF)。
  • 失败时,返回 -1,并设置全局变量 errno 以指示错误的具体原因。

行为

  • write 系统调用将尝试将 count 个字节的数据从缓冲区 buf 写入到文件描述符 fd 指向的文件中。
  • 如果写入成功,文件的当前偏移量(文件指针)会增加 count 个字节。
  • 如果文件是不可变的(例如,只读文件系统上的文件),write 调用将失败。
  • 如果写入的数据超过了文件的最大大小限制(例如,inode 描述的磁盘空间不足),write 调用也会失败。

错误

  • EACCES:没有写入文件的权限。
  • EBADF:文件描述符 fd 不是一个有效的文件描述符。
  • EFAULTbuf 指向的内存区域不可访问。
  • EFBIG:试图写入的数据超过了文件系统的最大文件大小。
  • EINTRwrite 调用被信号中断。
  • EIO:I/O 错误。
  • ENOSPC:文件系统没有足够的空间。
  • EROFS:试图在只读文件系统上写入。

注意事项

  • write 系统调用不会保证数据被物理写入到磁盘上,它只是将数据写入到内核缓冲区。数据可能会在之后被异步写入磁盘。
  • 如果需要确保数据被写入磁盘,可以使用 fsyncO_SYNC 标志。
  • write 调用可以写入的数据量可能受到多种因素的限制,包括文件系统的块大小、内核缓冲区的大小等。

用法

// myfile.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // close 要用
#include <string.h> // strlen 要用

int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); // O_WRONLY:只写入,O_CREAT:文件不存在就创建
    if (fd < 0)                                         // 如果失败的话,会返回-1
    {
        perror("open");
        return 1;
    }

    // 操作这个文件:
    const char *msg = "hello file system call \n";
    write(fd, msg, strlen(msg) + 1);

    close(fd);
    return 0;
}

运行上面的程序之后,我们打开被创建的log.txt 发现了有乱码:
在这里插入图片描述

📌 了解一些标志位

✏️ O_WRONLY

还有一件事: O_WRONLY ,以写的方式来打开文件,但是它并不会清空原来文件里的内容,它是以覆盖的方式来写的。
举个例子:
如果你原来文件的内容是:aaaa
你新写入的内容是:bbb
最终的结果是:bbba
原因就是:它是以覆盖的方式来写的,前面的 3 个 a 就被新的 3 个 b给覆盖了。

看下面的代码的例子:

// myfile.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // close 要用
#include <string.h> // strlen 要用

int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); // O_WRONLY:只写入,O_CREAT:文件不存在就创建
    if (fd < 0)                                         // 如果失败的话,会返回-1
    {
        perror("open");
        return 1;
    }

    // 操作这个文件:
    const char *msg = "aaaa";
    write(fd, msg, strlen(msg));

    close(fd);
    return 0;
}

运行上面的代码之后,log.txt 中的文件内容就是:aaaa

在这里插入图片描述

然后我们修改一下代码,把字符串改成bbb:

// myfile.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // close 要用
#include <string.h> // strlen 要用

int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); // O_WRONLY:只写入,O_CREAT:文件不存在就创建
    if (fd < 0)                                         // 如果失败的话,会返回-1
    {
        perror("open");
        return 1;
    }

    // 操作这个文件:
    const char *msg = "bbb";
    write(fd, msg, strlen(msg));

    close(fd);
    return 0;
}

我们来查看一下这时的结果:
在这里插入图片描述

✏️ O_TRUNC

好了,问题来了,如果我们想要打开这个文件的时候把这个文件里面的内容全部清空的话,我们要用的是一个新的选项---- O_TRUNC,他会将我们文件打开的长度清0,就是会先把文件清空,我们的代码如下:

  int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 

✏️ O_APPEND

除此之外我们还有一些其他的选项,比如:O_APPEND ,这个选项让我们可以在文件的结尾处开始写入,类似于追加,不会清空文件

int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); 

在这里插入图片描述

📌 文件的返回值,对打开文件的本质理解

通过上面的代码我们可以知道,文件的返回值十几个整数

// myfile.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // close 要用
#include <string.h> // strlen 要用

int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // O_WRONLY:只写入,O_CREAT:文件不存在就创建
    if (fd < 0)                                                    // 如果失败的话,会返回-1
    {
        perror("open");
        return 1;
    }

    printf("fd:%d\n", fd); // 我们可以把这个整数打印出来看一下
    
    // 操作这个文件:
    const char *msg = "ccc\n";
    write(fd, msg, strlen(msg));

    close(fd);
    return 0;
}

运行上面的代码,打印的结果是:fd = 3

为了更好的观察文件的返回值我们执行以下代码:

// myfile.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> 
#include <string.h> 

int main()
{
    int fda = open("loga.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); 
    int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); 
    int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); 
    int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); 
    
    printf("fda:%d\n", fda);
    printf("fdb:%d\n", fdb);
    printf("fdc:%d\n", fdc);
    printf("fdd:%d\n", fdd);

    return 0;
}

在这里插入图片描述

我们发现文件的返回值是一串连续的整数,是不是有点像数组的下标呀。

✏️ 理解文件在操作系统中的表现

一个文件要被访问必须先被打开,谁来打开这个文件呢,是进程来打开这个文件,所以我们研究文件的本质,是研究进程和文件之间的关系。

我们可以类比学习进程时操作系统创建的task_struct 来理解文件被打开时的状态,操作系统也会生成被打开文件的结构体描述对象struct_file

在这里插入图片描述

这里提出一个新的问题,我们怎么知道这些被打开的文件是属于哪一个进程的?换句话说就是:进程和被打开文件的对应关系是怎么样的❓

其实操作系统为进程设计了一个结构体用来解决上面👆🏻的问题,这个结构体叫做:struct files_struct,这个结构体里包含了一个数组,这是数组的类型是:struct file* 数组的名字叫做:fd_arry ,连起来就是:struct file* fd_arry[]

在这里插入图片描述

当我们使用 open 来打开一个文件的时候会发生以上行为:

  1. 会创建(malloc)一个 struct_file 的对象 。
  2. 把创建到底哪个文件对象的地址填入 fd_arry这个数组中。
  3. 将该数组的下标返回给上层。----- 这个数组的下标我们就叫文件描述符。

上面的👆🏻的那张数组表我们就叫做文件描述符表—全称:进程文件描述符表

在这里插入图片描述

✏️ 所以文件描述符的本质就是数组的下标,此时问题来了,观察我们上面的代码的运行结果:

我们发现,文件描述符是从 3 开始的,那他为什么不从 0 1 2 开始呢?

解答如下:进程在启动时会默认打开 3 个文件:标准输入(键盘)标准输出(显示器)标准错误(显示器) ,有同学会感到疑惑---- 难道键盘和显示器也是文件吗? 其实不用疑惑,因为 Linux 下一切皆是文件

标准输入键盘stdin0
标准输出显示器stdout1
标准错误显示器stderr2
所以0,1, 2 都被用掉了,我们新打开的文件就只能从 3 开始了

📌 理解一下 struct file 对象(内核对象)

struct file 里面要包含哪些内容?

  • 被打开文件的所有属性
  • 文件缓冲区

在这里插入图片描述

如果我们今天要读这个文件那么是谁在读?
答:进程,回想一下我们是怎么做的要么是 open 来打开文件 要么是 fopen来打开文件,这些都是进程干的事

我们看看下图来理一下思绪:

在这里插入图片描述

如果我们要读数据,
我们要先把磁盘中的数据加载到文件缓冲区中,所以我们要先将数据加载到内存

在这里插入图片描述

我们如果是写数据也同样需要将文件加载到内存中。
加载到内存这个动作是由操作系统来做的

我们在应用层进行数据读写的本质是什么?
答:本质是将内核缓冲区中的数据进行来回拷贝!

📌 fd 的分配规则

我们执行以下代码:

#include <stdio.h>
#include <sys/types.h> // open 函数要用
#include <sys/stat.h>  // open 函数要用
#include <fcntl.h>     // open 函数要用
#include <unistd.h>    // close 函数要用

#define FILE_NAME "log.txt"

int main()
{
    // O_CREAT : 文件不存在就创建 ,O_WRONLY: 以写的方式来打开文件, O_TRUNC: 清空这个文件之前的内容
    int fd = open(FILE_NAME, O_CREAT|O_WRONLY|O_TRUNC,0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    printf("fd:%d\n", fd);

    // 关闭这个文件
    close(fd);
    return 0;
}

打印的结果是:fd:3 符合我们的预期,因为0,1, 2 , 被标准输出,标准输入,标准错误给占据了,并且系统会默认给我们打开这三个文件。
现在问题来了,既然操作系统已经提前给我们打开了键盘显示器的文件,那我们是不是可以直接使用呢?

我们可以通过一个系统调用接口来实现我们的目的:read

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

好了,我们已经知道了 read 函数的用法,接下来的代码中会使用到 read 函数。

#include <stdio.h>
#include <sys/types.h> // open 函数要用
#include <sys/stat.h>  // open 函数要用
#include <fcntl.h>     // open 函数要用
#include <unistd.h>    // close 函数要用

#define FILE_NAME "log.txt"

int main()
{
    char buffer[1024];
    // 这个 buffer 就是对应的read 函数中的参数:buf。 1024是我们指定的buffer的大小
    // 0: 表示的是我们想要从标准输入中读取数据。
    // buffer : 然后将读取的数据存放进 buffer 中。
    // 1024:表示我们希望从标准输入中读取 1024 个字节的数据
    ssize_t s = read(0, buffer, 1024); 
    // s :表示的是我们实际上读取到的字节数,ssize_t 有符号整型
    if (s > 0) // 如果s > 0 说明读取到了数据,我们打印出来一下
    {
        buffer[s] = 0; // 我们把 buffer 当字符串来用,所以我们把这个数组的最后一位设为 0 ,就相当于字符串的 \0
        printf("echo#: %s\n", buffer);
    }
    return 0;
}

代码的运行结果:
在这里插入图片描述

在这里插入图片描述

这样的运行结果也证明了,我们确实可以直接的访问键盘相应文件的内容,既然我们可以不用 scanf 直接读取键盘输入的数据,那我们也可以不用 printf,直接向显示器中写入数据

此时,我们要用的函数是 write, 头文件:#include <unistd.h>

在这里插入图片描述

在这里插入图片描述

代码如下:

#include <stdio.h>
#include <sys/types.h> 
#include <sys/stat.h>  
#include <fcntl.h>     
#include <unistd.h>    
#include <string.h> // strlen 要用

#define FILE_NAME "log.txt"

int main()
{
    char buffer[1024];
    ssize_t s = read(0, buffer, 1024); 
    if (s > 0)                        
    {
        buffer[s] = 0;
        // printf("echo#: %s\n", buffer); 我们这里就不用 printf 来打印了,我们用 write
        write(1, buffer, strlen(buffer));
    }
    return 0;
}

代码运行结果:
在这里插入图片描述

在这里插入图片描述

  1. 通过上面的实验我们可以证明,进程默认打开了 0,1, 2 。我们可以直接使用 0,1,2进行数据的访问。
  2. 文件描述符的分配规则:寻找最小的没有被使用的数组的位置来分配给被打开的文件换句话说就是:如果你把 0 关闭了(close(0)),那么你打开一个新的文件时,它的 fd就是 0了,因为:寻找最小的没有被使用的数组的位置来分配给被打开的文件,你把 0 关闭之后,0 这个数组下标就没有被使用了,所以新的打开的文件就可以用 0 这个下标了

本章图集:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/918118.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

树的直径计算:算法详解与实现

树的直径计算:算法详解与实现 1. 引言2. 算法概述3. 伪代码实现4. C语言实现5. 算法分析6. 结论在图论中,树的直径是一个关键概念,它表示树中任意两点间最长路径的长度。对于给定的树T=(V,E),其中V是顶点集,E是边集,树的直径定义为所有顶点对(u,v)之间最短路径的最大值。…

RHCSA学习超详细知识点2命令篇

输入命令行的语法 终端中执行命令需要遵照一定的语法&#xff0c;输入命令的格式如下&#xff1a; 命令 参数命令 -选项 参数 输入命令时可以包含多个选项&#xff0c;假如一个命令有-a,-b,-c,-d四个选项&#xff0c;可以写作 命令 -a -b -c -d 参数 这里的多个选项可以“提…

3步实现贪吃蛇

方法很简单&#xff0c;打开页面&#xff0c;复制&#xff0c;粘贴 一.整体思维架构 我们根据游戏的开始&#xff0c;运行&#xff0c;结束&#xff0c;将整个游戏划分成三个部分。在每个部分下面又划分出多个功能&#xff0c;接下来我们就根据模块一一实现功能。 二.Gamesta…

风电电力系统低碳调度论文阅读第一期

在碳交易市场中&#xff0c;历史法和基准线法是用于分配碳排放配额的两种主要方法。以下是两种方法的公式及其解释&#xff1a; 区别总结 历史法&#xff1a;基于历史排放量&#xff0c;分配具有较强的公平性但可能缺乏激励减排。基准线法&#xff1a;基于行业基准和生产量&am…

Mybatis-Plus 多租户插件属性自动赋值

文章目录 1、Mybatis-Plus 多租户插件1.1、属性介绍1.2、使用多租户插件mavenymlThreadLocalUtil实现 定义,注入租户处理器插件测试domianservice & ServiceImplmapper 测试mapper.xml 方式 1.3、不使用多租户插件 2、实体对象的属性自动赋值使用1. 定义实体类2. 实现 Meta…

CSS基础知识05(弹性盒子、布局详解,动画,3D转换,calc)

目录 0、弹性盒子、布局 0.1.弹性盒子的基本概念 0.2.弹性盒子的主轴和交叉轴 0.3.弹性盒子的属性 flex-direction row row-reverse column column-reverse flex-wrap nowrap wrap wrap-reverse flex-dirction和flex-wrap的组合简写模式 justify-content flex-s…

使用Web Animations API实现复杂的网页动画效果

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用Web Animations API实现复杂的网页动画效果 使用Web Animations API实现复杂的网页动画效果 使用Web Animations API实现复杂…

Matlab多输入单输出之倾斜手写数字识别

本本主要介绍使用matlab构建多输入单输出的网络架构&#xff0c;来实现倾斜的手写数字识别&#xff0c;使用concatenationLayer来拼接特征&#xff0c;实现网络输入多个特征。 1.加载训练数据 加载数据&#xff1a;手写数字的图像、真实数字标签和数字顺时针旋转的角度。 lo…

pytest结合allure做接口自动化

这是一个采用pytest框架&#xff0c;结合allure完成接口自动化测试的项目&#xff0c;最后采用allure生成直观美观的测试报告&#xff0c;由于添加了allure的特性&#xff0c;使得测试报告覆盖的内容更全面和阅读起来更方便。 1. 使用pytest构建测试框架&#xff0c;首先配置好…

【无人机设计与控制】基于MATLAB的四旋翼无人机PID双闭环控制研究

摘要 本文基于MATLAB/Simulink环境&#xff0c;对四旋翼无人机进行了PID双闭环控制设计与仿真研究。通过分析四旋翼无人机的动力学模型与运动学模型&#xff0c;建立了姿态和位置双闭环控制系统&#xff0c;以实现无人机的稳定飞行与精确轨迹跟踪。仿真实验验证了该控制策略的…

强大的正则表达式——Easy

进入题目界面输入难度1后&#xff0c;让我们输入正则表达式&#xff08;regex&#xff09;&#xff1a; 目前不清楚题目要求&#xff0c;先去下载附件查看情况&#xff1a; import re import random# pip install libscrc import libscrcallowed_chars "0123456789()|*&q…

pytest | 框架的简单使用

这里写目录标题 单个文件测试方法执行测试套件的子集测试名称的子字符串根据应用的标记进行选择 其他常见的测试命令 pytest框架的使用示例 pytest将运行当前目录及其子目录中test_*.py或 *_test.py 形式的所有 文件 文件内的函数名称可以test* 或者test_* 开头 单个文件测试…

【安卓恶意软件检测-论文】DroidEvoler:自我进化的 Android 恶意软件检测系统

DroidEvolver&#xff1a;自我进化的 Android 恶意软件检测系统 摘要 鉴于Android框架的频繁变化和Android恶意软件的不断演变&#xff0c;随着时间的推移以有效且可扩展的方式检测恶意软件具有挑战性。为了应对这一挑战&#xff0c;我们提出了DroidEvolver&#xff0c;这是一…

Vulnhub靶场 Billu_b0x 练习

目录 0x00 准备0x01 主机信息收集0x02 站点信息收集0x03 漏洞查找与利用1. 文件包含2. SQL注入3. 文件上传4. 反弹shell5. 提权&#xff08;思路1&#xff1a;ssh&#xff09;6. 提权&#xff08;思路2&#xff1a;内核&#xff09;7. 补充 0x04 总结 0x00 准备 下载链接&#…

LabVIEW弧焊参数测控系统

在现代制造业中&#xff0c;焊接技术作为关键的生产工艺之一&#xff0c;其质量直接影响到最终产品的性能与稳定性。焊接过程中&#xff0c;电流、电压等焊接参数的精确控制是保证焊接质量的核心。基于LabVIEW开发的弧焊参数测控系统&#xff0c;通过实时监控和控制焊接过程中关…

CentOS网络配置

上一篇文章&#xff1a;VMware Workstation安装Centos系统 在CentOS系统中进行网络配置是确保系统能够顺畅接入网络的重要步骤。本文将详细介绍如何配置静态IP地址、网关、DNS等关键网络参数&#xff0c;以帮助需要的人快速掌握CentOS网络配置的基本方法和技巧。通过遵循本文的…

低速接口项目之串口Uart开发(一)——串口UART

本节目录 一、串口UART 二、串口协议 三、串口硬件 四、往期文章链接本节内容 一、串口UART 串口UART,通用异步收发传输器&#xff08;Universal Asynchronnous Receiver / Transmitter&#xff09;,一种异步收发传输器&#xff0c;全双工传输。数据发送时&#xff0c;将并行…

Uni-APP+Vue3+鸿蒙 开发菜鸟流程

参考文档 文档中心 运行和发行 | uni-app官网 AppGallery Connect DCloud开发者中心 环境要求 Vue3jdk 17 Java Downloads | Oracle 中国 【鸿蒙开发工具内置jdk17&#xff0c;本地不使用17会报jdk版本不一致问题】 开发工具 HBuilderDevEco Studio【目前只下载这一个就…

SQL 外连接

1 外连接 外连接是一种用于结合两个或多个表的方式&#xff0c;返回至少一个表中的所有记录。 左外连接 LEFT JOIN&#xff0c;左表为驱动表&#xff0c;右表为从表。返回驱动表的所有记录以及从表中的匹配记录。如果从表没有匹配&#xff0c;则结果中从表的部分为NULL。 右…

笔记|M芯片MAC (arm64) docker上使用 export / import / commit 构建amd64镜像

很简单的起因&#xff0c;我的东西最终需要跑在amd64上&#xff0c;但是因为mac的架构师arm64&#xff0c;所以直接构建好的代码是没办法跨平台运行的。直接在arm64上pull下来的docker镜像也都是arm64架构。 检查镜像架构&#xff1a; docker inspect 8135f475e221 | grep Arc…