Linux学习_系统文件IO

Posted Leslie X徐

tags:

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

系统文件IO

C程序文件IO结构

typedef struct iobuf
{
	int cnt;	/*剩余的字节数*/
	char *ptr;	/*下一个字符的位置*/
	char *base;	/*缓冲区的位置*/
	int flag;	/*文件访问模式*/
	int fd;	/*文件描述符*/
}FILE;

标准C的IO缓存类型

  1. 缓存类型
  • 全缓存
    • 要求填满整个缓存区后才进行I/O系统调用操作。对于磁盘文件通常使用全缓存访问。
  • 行缓存
    • 涉及一个终端时(例如标准输入输出),使用行缓存。
    • 行缓存退出条件:
      1. 碰到换行符’\\n’
      2. 行缓存满,大概几千KB
      3. 程序结束
      4. 调用fflush(FILE*)刷新缓存
    • 全缓存的退出条件为以上的2,3,4
  • 无缓存
    • 标准错误流stderr通常是不带缓存区的,这使得错误信息能够尽快地显示出来。
  1. 行缓存案例:
/*
 * 行缓存.c
 */
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
	printf("hello show\\n"); //有输出
	printf("hello not show"); //无输出
	fflush(stdout);
	while(1)sleep(1); //若没有while则程序结束时系统会自动清空行缓存,强制输出
	return 0;
}
/*通过2个printf分别将hello输出到文件中,world输出到终端
 */
 #

  • 分析:标准输出使用行缓存,遇到换行符会自动输出,行缓存满也会自动输出,反之没有换行符,行缓存也不满(程序一直在循环),则不会输出。
  • printf()最好最后都加’\\n’

Linux 文件I/O系统调用

对于文件的操作,都是通过指针去操作文件的
c语言中直接给了一个FILE*的指针去指向文件的地址然后进行操作
在lsd当中,所有与该程序相关的文件的地址,他们的描述符都会保存在一个数组当中(该数组我们在程序中是不可见的)
所以,我们要访问相关文件的话,只需要访问他们的下标就可以了
所以说,lsd当中的文件描述符是int类型
感觉类似如下:有3个打开的文件 f1,f2,f3
用c语言去打开的话就是3个指针 fp1,fp2,fp3
想要操作f1的话,直接针对fp1进行操作
用lsd打开的话,打开的瞬间会有如下操作 FILE* file[20] = {0};
file[3] = f1,file[4] = f2,file[5] = f3;
想要操作f2的话,直接针对 4 进行操作
任何一个新的程序,他如果打开一个新的文件的话,他的文件描述符总是从3开始
为什么从3开始?因为 0,1,2分表代表了
标准输入流, 标准输出流, 标准错误流
STDIN_FILENO STDOUT_FILENO STDERR_FILENO
scanf printf perror
注意:

  • stdin等是FILE *类型,属于标准I/O,在<stdio.h>。
  • STDIN_FILENO等是文件描述符,是非负整数,一般定义为0,1,2,属于没有buffer的I/O,直接调用系统调用,在<unistd.h>。
    lsd当中的文件io操作的函数有:
    open,close,write,read

一、 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);

  • 返回:若成功返回文件描述符,若出错为-1
  • 功能:打开或创建一个文件
  • 参数:
    • pathname:要打开或创建的文件路径
    • flags:用来说明此函数的多个选择项(以下宏定义都定义在fcntl.h)
      • O_RDONLY:只读
      • O_WRONLY:只写
      • O_RDWR:读写
    • mode:新文件的访问权限,对于open函数而言,仅当创建新文件时才使用第三个参数。

二、creat函数

int creat(const char *pathname, mode_t mode);

返回: 若成功为只写打开的文件描述符,若出错为-1
功能: 创建一个新文件
等同于open (pathname, O_WRONLY | O_CREAT | O_TRUNC, mode) ;
以只写方式打开所创建的文件。

三、close函数

int close(int fd);

返回: 若成功为0,若出错为-1
功能: 关闭一个打开的文件
参数
fd:已打开文件的文件描述符

四、lseek函数

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

off_t lseek(int fd, off_t offset, int whence);

操作类似于fseek。注意文件最后一行结束会隐藏有’\\0’和EOF
示例:

char buf[20]={0};
int fd=open("demo.txt",O_RDONLY);

lseek(fd,6,SEEK_SET);
read(fd,buf,5);
printf("读取到的数据为:%s\\n",buf);

五、read()和write()函数

 #include <unistd.h>

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

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

六、管道函数popen,pipe和FIFO

  • 管道文件

    • 在程序中使用带参宏S_ISFIFO()表示

    • 作用:在两个进程间进行数据交互

    • 分类:

      • 匿名管道pipe:由于在硬盘中不存在文件,只在程序中存在(指没有路径及文件名只有文件描述符)。匿名管道只允许在拥有亲缘关系(父子关系)的进程中使用。
      • 命名管道FIFO:存在于硬盘中,拥有绝对路径及文件名,其可以在两个任意的进程中使用。
    • 管道自带同步机制(按照严格的既定顺序运行程序)。
      场景:
      有2个进程,1为输入内容,2为读取内容
      首先1以写端打开管道,2为读打开管道
      此时如果1先打开open,则open会阻塞。直到2号打开才会解除阻塞
      若2先打开管道,不会堵塞,但read会阻塞,等1write完成解除

    一根管道是单向的,只能是一端读一端写

    1.popen():

    FILE * popen ( const char * command , const char * type );
    int pclose ( FILE * stream );
    
    • popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。这个进程必须由 pclose() 函数关闭,而不是 fclose() 函数。pclose() 函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。如果 shell 不能被执行,则 pclose() 返回的终止状态与 shell 已执行 exit 一样。
    • type 参数只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。
    • command 参数是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用-c 标志,shell 将执行这个命令。
    • popen的返回值是个标准I/O流,必须由pclose来终止。前面提到这个流是单向的。所以向这个流写内容相当于写入该命令的标准输入;命令的标准输出和调用 popen 的进程相同。与之相反的,从流中读数据相当于读取命令的标准输出;命令的标准输入和调用 popen 的进程相同。

2.pipe:

	 #include <unistd.h>
	int pipe(int pipefd[2]);

例子:

EXAMPLE
       The following program creates a pipe, and then fork(2)s to create a child process; the child inherits  a  duplicate
       set  of  file descriptors that refer to the same pipe.  After the fork(2), each process closes the file descriptors
       that it doesn't need for the pipe (see pipe(7)).  The parent then writes the string contained in the program's com‐
       mand-line  argument  to  the  pipe, and the child reads this string a byte at a time from the pipe and echoes it on
       standard output.

   Program source
       #include <sys/types.h>
       #include <sys/wait.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <string.h>

       int
       main(int argc, char *argv[])
       {
           int pipefd[2];
           pid_t cpid;
           char buf;

           if (argc != 2) {
               fprintf(stderr, "Usage: %s <string>\\n", argv[0]);
               exit(EXIT_FAILURE);
           }

           if (pipe(pipefd) == -1) {
               perror("pipe");
               exit(EXIT_FAILURE);
           }

           cpid = fork();
           if (cpid == -1) {
               perror("fork");
               exit(EXIT_FAILURE);
           }

           if (cpid == 0) {    /* Child reads from pipe */
               close(pipefd[1]);          /* Close unused write end */

               while (read(pipefd[0], &buf, 1) > 0)
                   write(STDOUT_FILENO, &buf, 1);

               write(STDOUT_FILENO, "\\n", 1);
               close(pipefd[0]);
               _exit(EXIT_SUCCESS);

           } else {            /* Parent writes argv[1] to pipe */
               close(pipefd[0]);          /* Close unused read end */
               write(pipefd[1], argv[1], strlen(argv[1]));
               close(pipefd[1]);          /* Reader will see EOF */
               wait(NULL);                /* Wait for child */
               exit(EXIT_SUCCESS);
           }
       }

3.FIFO命名管道,在不同进程间的管道,使用配合open使用

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

      int mkfifo(const char *pathname, mode_t mode);

文件描述符重定向

关于文件描述符重定向是指:

  • 将一个已经有了确定流向的文件描述符重新定位他的流向

  • 在终端上,可以通过 1>file 更改标准输入流的流向,file代表最终流向的文件

  • 还有一种写法 1>&2 将标准输出流流向标准错误流,至于标准错误流流向哪里,不关心

    针对指令 echo 1>&2 2>err.txt 操作
    他的本质操作如下:
    file[1] = file[2];
    file[2] = err.txt;

    针对 echo 2>err.txt 1>&2
    本质操作如下
    file[2] = err.txt
    file[1] = file[2]

    在程序中如何重定向:使用dup2()

    • int dup2(int oldfd, int newfd);
      本质操作:
      file[newfd]=file[oldfd];

    注意当标准输出流重定向之后,printf函数就会由行缓存变为全缓存

示例:

/*
 * 2个printf输出不同file.c
 * 通过2个printf分别将hello输出到文件中,world输出到终端
 */


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


int main(int argc, char **argv)
{
	int fd = open("./test1.txt",O_WRONLY | O_CREAT | O_TRUNC, 0664 );
	
	int std_out = dup(STDOUT_FILENO); //复制stdout文件描述符给std_out
	printf("%d\\n",std_out);
	dup2( fd, STDOUT_FILENO); //file[STDOUT_FILENO]=file[fd],将输出文件重定向为fd文件,print输出将输出到fd文件
	//注意重定向后文件成为全缓存模式,不可使用'\\n'直接输出
	printf("hello");
	fflush(stdout); //手动清除缓存区,防止下一步和world一起输出
	close(fd);
	
	dup2( std_out, STDOUT_FILENO ); //file[STDOUT_FILENO]=file[std_out],将输出文件重定向为std_out文件即原来的,print输出将输出到fd文件
	printf("world\\n");
	
	return 0;
}

struct termios结构体详解

一、数据成员
termios 函数族提供了一个常规的终端接口,用于控制非同步通信端口。 这个结构包含了至少下列成员:

tcflag_t c_iflag;      /* 输入模式 */
tcflag_t c_oflag;      /* 输出模式 */
tcflag_t c_cflag;      /* 控制模式 */
tcflag_t c_lflag;      /* 本地模式 */
cc_t c_cc[NCCS];       /* 控制字符 */

struct termios
{unsigned short c_iflag; /* 输入模式标志*/
unsigned short c_oflag; /* 输出模式标志*/
unsigned short c_cflag; /* 控制模式标志*/
unsigned short c_lflag; /*区域模式标志或本地模式标志或局部模式*/
unsigned char c_line; /*行控制line discipline */
unsigned char c_cc[NCC]; /* 控制字符特性*/
};

二、作用
这个变量被用来提供一个健全的线路设置集合, 如果这个端口在被用户初始化前使用. 驱动初始化这个变量使用一个标准的数值集, 它拷贝自 tty_std_termios 变量. tty_std_termos 在 tty 核心被定义为:

struct termios tty_std_termios = {
 .c_iflag = ICRNL | IXON,
 .c_oflag = OPOST | ONLCR,
 .c_cflag = B38400 | CS8 | CREAD | HUPCL,
 .c_lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK |
 ECHOCTL | ECHOKE | IEXTEN,
 .c_cc = INIT_C_CC
};

这个 struct termios 结构用来持有所有的当前线路设置, 给这个 tty 设备的一个特定端口. 这些线路设置控制当前波特率, 数据大小, 数据流控设置, 以及许多其他值.

.c_lflag 标志常量:Local mode ( 局部模式)
Local mode主要用来控制终端设备不同的特色。利用termios结构里的c_lflag的标志来设定局部模式。
在巨集中有两个比较重要的标志:
1.ECHO:它可以让你阻止键入字元的回应。
2.ICANON(正规模式)标志,它可以对所接收的字元在两种不同的终端设备模式之间来回切换。
其他标志
ISIG:当接受到字符 INTR, QUIT, SUSP, 或 DSUSP 时,产生相应的信号。
ICANON:启用标准模式 (canonical mode)。允许使用特殊字符 EOF, EOL, EOL2, ERASE, KILL, LNEXT, REPRINT, STATUS, 和 WERASE,以及按行的缓冲。
XCASE:(不属于 POSIX; Linux 下不被支持) 如果同时设置了 ICANON,终端只有大写。输入被转换为小写,除了有前缀的字符。输出时,大写字符被前缀(某些系统指定的特定字符) ,小写字符被转换成大写。
ECHO :回显输入字符。
ECHOE :如果同时设置了 ICANON,字符 ERASE 擦除前一个输入字符,WERASE 擦除前一个词。
ECHOK :如果同时设置了 ICANON,字符 KILL 删除当前行。
ECHONL :如果同时设置了 ICANON,回显字符 NL,即使没有设置 ECHO。
ECHOCTL :(不属于 POSIX) 如果同时设置了 ECHO,除了 TAB, NL, START, 和 STOP 之外的 ASCII 控制信号被回显为 ^X, 这里 X 是比控制信号大 0x40 的 ASCII 码。例如,字符 0x08 (BS) 被回显为 ^H。
ECHOPRT :(不属于 POSIX) 如果同时设置了 ICANON 和 IECHO,字符在删除的同时被打印。
ECHOKE :(不属于 POSIX) 如果同时设置了 ICANON,回显 KILL 时将删除一行中的每个字符,如同指定了 ECHOE 和 ECHOPRT 一样。
DEFECHO :(不属于 POSIX) 只在一个进程读的时候回显。
FLUSHO :(不属于 POSIX; Linux 下不被支持) 输出被刷新。这个标志可以通过键入字符 DISCARD 来开关。
NOFLSH :禁止在产生 SIGINT, SIGQUIT 和 SIGSUSP 信号时刷新输入和输出队列,即关闭queue中的flush。
TOSTOP :向试图写控制终端的后台进程组发送 SIGTTOU 信号(传送欲写入的信息到后台处理)。
PENDIN :(不属于 POSIX; Linux 下不被支持) 在读入下一个字符时,输入队列中所有字符被重新输出。(bash 用它来处理 typeahead)
IEXTEN :启用实现自定义的输入处理。这个标志必须与 ICANON 同时使用,才能解释特殊字符 EOL。

四、与此结构体相关的函数

(一)tcgetattr()
1.原型
int tcgetattr(int fd,struct termois & termios_p);
2.功能
取得终端介质(fd)初始值,并把其值 赋给temios_p;函数可以从后台进程中调用;但是,终端属性可能被后来的前台进程所改变。

(二)tcsetattr()
1.原型
int tcsetattr(int fd,int actions,const struct termios *termios_p);
2.功能
设置与终端相关的参数 (除非需要底层支持却无法满足),使用 termios_p 引用的 termios 结构。optional_actions (tcsetattr函数的第二个参数)指定了什么时候改变会起作用:
TCSANOW:改变立即发生
TCSADRAIN:改变在所有写入 fd 的输出都被传输后生效。这个函数应当用于修改影响输出的参数时使用。(当前输出完成时将值改变)
TCSAFLUSH :改变在所有写入 fd 引用的对象的输出都被传输后生效,所有已接受但未读入的输入都在改变发生前丢弃(同TCSADRAIN,但会舍弃当前所有值)。

示例:

#include<stdio.h>
#include<unistd.h>
#include<termios.h>
#include<assert.h>
#include<string.h>
char getch(){  
		char c=0; 
		struct termios org_opts;
		struct termios new_opts;
		int res=0;  
		res=tcgetattr(STDIN_FILENO, &org_opts);
		assert(res==0);
		//memcpy(&new_opts, &org_opts, sizeof(org_opts));
		new_opts = org_opts;//结构体变量互相赋值
		new_opts.c_lflag &=~(ICANON | ECHO | ECHOE | ECHOK | ECHONL | ECHOPRT     | ECHOKE | ICRNL); 
		tcsetattr(STDIN_FILENO, TCSANOW, &new_opts);
		c=getchar();  
		res=tcsetattr(STDIN_FILENO, TCSANOW, &org_opts);
		assert(res==0);  
		return c;  
}

dup()和dup2()

dup()和dup2()函数都可以用来复制一个文件描述符,原型为:

int dup(int oldfd);
int dup2(int oldfd, int newfd);

函数执行成功返回新的文件描述符,失败则返回-1。
dup()函数返回的新的文件描述符是当前可用文件描述符中最小数值
示例:

int main(void)
{
    int fd;
    int new_fd;

    fd = open("./test.file", O_RDWR | O_CREAT | O_TRUNC, 0666);
    printf("fd = %d\\n", fd);

    new_fd = dup(fd);    //复制文件描述符,把fd复制给new_fd
    printf("new_fd = %d\\n", new_fd);
    write(new_fd, "hello", 5);

    close(fd);
    close(new_fd);

    return 0;
}

复制文件描述符,实质应该理解为:

  • fd句柄原本指向test.file的文件描述结构体,dup()指向完毕后,new_fd句柄也会指向test.file文件描述结构体。所以说对new_fd句柄所指的文件操作,等价于操作test_file文件。

dup2()与dup()的区别:

  • 区别在于可以用newfd来指定新描述符数值,若newfd指向的文件已经被打开,会先将其关闭。若newfd等于oldfd,就不关闭newfd,newfd和oldfd共同指向一份文件。
    dup2()示例:
int main(void)
{
    int fd;
    int new_fd;

    fd = open("./test.file", O_RDWR | O_CREAT | O_TRUNC, 0666);
    printf("fd = %d\\n", fd);

    dup2(fd, 1);
    printf("hello world!!\\n");

    return 0;
}

dup2(fd,1)的意思是,newfd指向oldfd句柄指向的文件描述符结构,即原本是指向标准输出文件描述结构体的1指向了test.file,这样一来,原本输出到显示器终端的字符串就打印到test.file文件中了,这也是Linux操作系统的重定向实现方法。

以上是关于Linux学习_系统文件IO的主要内容,如果未能解决你的问题,请参考以下文章

Linux学习基础IO——系统调用 | 文件描述符fd | 重定向

Linux下的IO-one简述

linux内核文件IO的系统调用实现分析(open)

Linux学习基础IO——理解缓冲区 | 理解文件系统

Linux系统编程学习问题回顾

Linux系统编程:基础IO 上简单复习C语言文件接口 | 学习系统文件接口 | 认识文件描述符 | Linux下,一切皆文件 | 重定向原理