Linux的系统调用open,write,read,close,及相关总结

时间:2021-12-26 19:07:47

在进行C语言学习的时候我们了解到了C语言相关的一些IO操作,如fopen,fwrite,fread,fprintf,fclose等相关函数,他们都是由C库函数提供的一些函数,是将操作系统的系统调用加以封装,虽说Linux是由C语言实现的,但为了使我们更加的了解Linux,就需要了解更接近与底层的一些IO操作,因此就需要来了解下基本的系统调用—open,write,read,close

首先我们来了解下open,write,read,close的系统调用

open

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
  • 1
  • 2
  • 3
  • 4
  • 5

open有三个参数 
pathname:要打开或创建的目标文件名 
flags:对文件进行多种操作也就有有多个参数,这多个参数可以进行或运算,即就是flags 
参数:

  1. O_RDONLY:只读打开
  2. O_WRONLY:只写打开
  3. O_RDWR:读,写打开
  4. O_CREAT:若文件不存在,创建文件
  5. O_APPEND:追加写

参数1,2,3,必须制定一个且只能制定一个,使用参数4,必须使用open的第三个参数mode:新文件的访问权限

返回值:成功:新打开文件的文件描述符(fd) 
失败:-1

write

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • 1
  • 2
  • 3
  • 4

fd:文件描述符 
buf:写入的缓冲区 
count:写的字符长度,也就是看你需要写多少

read

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

  • 1
  • 2
  • 3
  • 4

read的参数和write的参数很相像,只有第二个参数的含义有些不一样,它的buf是需要读的缓冲区

close

#include <unistd.h>
int close(int fd);

  • 1
  • 2
  • 3
  • 4

close的参数就相对简单了,这一个操作是不能遗漏的,只要了使用fd就必须close它

在这几个函数中都涉及到了关键的参数fd,因此要了解这几个函数,就必须先了解下文件描述符(fd)。

什么是文件描述符,这是一个相对抽象的概念,我们先来看看下面这张图

Linux的系统调用open,write,read,close,及相关总结

在PCB结构体中存在一个files指针,它指向一个file_struct结构体,而在file_struct结构体中存在一个file* fd数组,这个数组里面存放的是file指针,用来指向不同的file文件,而fd就可以理解为这个指针数组的下标,因此要打开一个文件,我们就可以拿到该文件的fd就可以了。

fd的分配原则: 
在files_struct数组当中,使用没有被使用的最小下标,作为新的文件描述符。 
操作系统默认使用了该数组的前三个元素,0号下标指向标准输入(stdin),1号下标指向标准输出(stdout),2号下标指向标准错误(stderr)。 
因此正常情况下,新的fd都是从3开始的,但如果我们关闭了默认的fd,新的文件的fd就从关闭的fd处开始。

说到了fd,我们就不得不来区分下FILE和fd

FILE是C库当中提供的一个结构体,而fd是系统调用,更加接近于底层,因此FILE中必定封装了fd。

我们可以来看看FILE的结构体: 
typedef struct _IO_FILE FILE;在/usr/include/stdio.h

它的结构体中有这么一段:

struct _IO_FILE {
  int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

//缓冲区相关
  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;   /* Current read pointer */
  char* _IO_read_end;   /* End of get area. */
  char* _IO_read_base;  /* Start of putback+get area. */
  char* _IO_write_base; /* Start of put area. */
  char* _IO_write_ptr;  /* Current put pointer. */
  char* _IO_write_end;  /* End of put area. */
  char* _IO_buf_base;   /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */ 
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;//fd的封装
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

可以看到int_fileno就是对fd的封装,在这一部分的开头有一大段跟缓冲区相关的内容,为什么要诺列出它呢,首先可以来看一个很诡异的例子:

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

  int main(){
      const char *msg1 = "hello printf\n";
      const char *msg2 = "hello fwrite\n";
      const char *msg3 = "hello write\n";

      printf(msg1);
      fwrite(msg2, 1, strlen(msg2), stdout);
      write(1, msg3, strlen(msg3));
      fork();
      return 0;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

运行结果: 
[rlh@localhost test]$ ./hello 
hello printf 
hello fwrite 
hello write

但当我们对进程实现输出重定向,你就会发现诡异的事情: 
[rlh@localhost test]$ ./hello > file

[rlh@localhost test]$ cat file 
hello write 
hello printf 
hello fwrite 
hello printf 
hello fwrite

这是为什么呢,这是跟C库的缓冲数据有关,C库缓冲数据分为三种(1)、无缓冲(2)、行缓冲(3)、全缓冲。 
行缓冲就是往显示器上写,全缓冲就是往文件里写。 
在上面的现象中,write不受影响是因为它属于系统调用,没有缓冲区,而printf和fwrite会自带缓冲区,当发生重定向到普通文件的时候,它就会从行缓冲转变为全缓冲,也就是会往文件里面写写,但是我们缓冲区里的数据,即使fork也不会立即被刷新,当进程退出后会统一刷新,写入文件,但是fork的时候会发生写时拷贝,也就是当父进程准备刷新的时候,子进程就已经有了一份相同的数据,所以就会产生上面的现象。

了解下重定向。 
重定向分为三种:

  1. 输出重定向(>) 也就是关闭fd为1下标所指向的内容
  2. 输入重定向(<) 同理就是关闭fd为0下标所指向的内容
  3. 追加重定向(>>) 后面多一个追加选项