Linux:基础IO

回顾C文件接口

stdin & stdout & stderr

C 默认会打开三个输入输出流,分别是 stdin, stdout, stderr
仔细观察发现,这三个流的类型都是 FILE*, fopen 返回值类型,文件指针

系统文件I/O

接口介绍

open

man 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 : 追加写

O_TRUNC:打开文件时会清空文件内容
返回值:
成功:新打开的文件描述符
失败: - 1 

mode_t 理解:直接 man 手册,比什么都清楚。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要 open 创建,则第三个参数表示创建文件的默认权限, 否则,使用两个参数的 open
hello.c 写文件 :
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
	umask(0);
	int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	int count = 5;
	const char* msg = "hello bit!\n";
	int len = strlen(msg);
	while (count--) {
		write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
		//默认不会清空文件内容,从头开始写
	}
	close(fd);
	return 0;
}
hello.c 读文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
	int fd = open("myfile", O_RDONLY);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	const char* msg = "hello bit!\n";
	char buf[1024];
	while (1) {
		ssize_t s = read(fd, buf, strlen(msg));//类比write
		if (s > 0) {
			printf("%s", buf);
		}
		else {
			break;
		}
	}
	close(fd);
	return 0;
}

open函数返回值

在认识返回值之前,先来认识一下两个概念 : 系统调用 库函数
上面的 fopen fclose fread fwrite 都是 C 标准库当中的函数,我们称之为库函数( libc )。
而  open close read write lseek 都属于系统提供的接口,称之为系统调用接口
回忆一下我们讲操作系统概念时,画的一张图
系统调用接口和库函数的关系,一目了然。
所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发。

文件描述符fd

通过对 open 函数的学习,我们知道了文件描述符就是一个小整数
文件描述符fd的本质是内核的进程中文件映射关系的数组的下标
理解:在进程中每打开一个文件,都会创建有相应的文件描述信息struct file,这个描述信息被添加在pcb的struct files_struct中,以数组的形式进行管理,随即向用户返回数组的下标作为文件描述符,用于操作文件

 0 & 1 & 2

Linux 进程默认情况下会有 3 个缺省打开的文件描述符,分别是标准输入 0 , 标准输出 1, 标准错误 2.
0,1,2 对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
	char buf[1024];
	ssize_t s = read(0, buf, sizeof(buf));
	if (s > 0) {
		buf[s] = 0;
		write(1, buf, strlen(buf));
		write(2, buf, strlen(buf));
	}
	return 0;
}

而现在知道,文件描述符就是从 0 开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表 files_struct, 该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

理解stderr和标准输出重定向>

文件描述符的分配规则

直接看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	int fd = open("myfile", O_RDONLY);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}
输出发现是 fd: 3
关闭 0 或者 2 ,在看
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	close(0);
	//close(2);
	int fd = open("myfile", O_RDONLY);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}
发现是结果是: fd: 0 或者 (//) fd:2 可见,文件描述符的分配规则:在 files_struct 数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

用库函数而不用系统调用的原因

系统调用:代码不具备跨平台性

重定向

那如果关闭1呢?看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
 close(1);
 int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
 if(fd < 0){
 perror("open");
 return 1;
 }
 printf("fd: %d\n", fd);
 fflush(stdout);//刷新语言级别的文件缓冲区,将数据刷新到内核文件缓冲区中
 
 close(fd);
 return 0;
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中, fd 1 。这种现象叫做输出重定向。常见的重定向有:>, >>, <
那重定向的本质是什么呢?

 使用 dup2 系统调用

函数原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);
它的作用是将 oldfd 所指的文件描述符复制到 newfd,并且返回 newfd。如果 newfd 已经打开,则先关闭 newfd,再进行复制。该函数成功时返回新的文件描述符,失败时返回 -1,并设置相应的错误码。

原理解析
dup2 函数的实现原理可以简要概括如下:

首先,检查 newfd 是否合法。如果 newfd 等于 oldfd,则直接返回 newfd。

然后,检查 newfd 是否已经打开。如果已经打开,则关闭 newfd。

调用系统调用 dup2(oldfd, newfd) 完成文件描述符的复制。该系统调用会将 newfd 关联到与 oldfd 相同的文件,使得它们指向相同的文件表项。

最后,返回 newfd。

注意事项和常见问题
在使用 dup2 函数时,需要注意以下几点:

传递给 dup2 的两个文件描述符必须是有效的。否则,函数调用将失败并返回 -1。

使用 dup2 之前,最好先关闭 newfd,以避免文件描述符泄露和资源浪费。

dup2 函数并不会关闭 oldfd,因此在复制完成后,需要根据实际需求手动关闭 oldfd。

示例代码:

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

const char* filename = "log.txt";

int main()
{
    //int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    dup2(fd, 1);
    printf("hello world\n");
    fprintf(stdout, "hello world\n");
    fflush(stdout);
    close(fd);

    return 0;
}

 FILE

因为 IO 相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过 fd 访问的。所以C库当中的FILE结构体内部,必定封装了fd
来段代码在研究一下:
#include <stdio.h>
#include <string.h>
int main()
{
	const char* msg0 = "hello printf\n";
	const char* msg1 = "hello fwrite\n";
	const char* msg2 = "hello write\n";
	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	write(1, msg2, strlen(msg2));
	fork();
	return 0;
}
运行出结果:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我们发现 printf fwrite (库函数)都输出了 2 次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!
一般 C 库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf   fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
而我们放在缓冲区中的数据,就不会被立即刷新,甚至 fork 之后但是进程退出之后,会统一刷新,写入文件当中。
但是 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
write 没有变化,说明没有所谓的缓冲
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS 也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的 上层 , 是对系统调用的“ 封装 ,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由 C 标准库提供。
C语言为什么要在FILE中提供用户级缓冲区---为了减少底层调用系统调用的次数,让使用C IO函数(printf、fprintf)效率更高
如果有兴趣,可以看看 FILE 结构体:
typedef struct _IO_FILE FILE ; /usr/include/stdio.h
/usr/include/libio.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; // 封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

简述重定向的实现原理

每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件。

理解文件系统

我们使用 ls -l 的时候看到的除了看到文件名,还看到了文件元数据。
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9 13 14:56" test.c
每行包含 7 列:
模式
硬链接数
文件所有者
大小
最后修改时间
文件名
ls -l 读取存储在磁盘上的文件信息,然后显示出来

其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息

[root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 普通文件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800

上面的执行结果有几个信息需要解释清楚

inode

inode(Index Node,索引节点)是文件系统中的一种数据结构(结构体),用于存储文件或目录的元数据信息。每个文件或目录都与一个唯一的inode相关联,该inode包含了关于文件或目录的诸多属性,如文件大小、文件类型、权限、拥有者、创建时间、修改时间等。此外,inode还存储了文件数据的物理位置信息,例如文件数据存储在磁盘上的哪个扇区。

当我们在Linux系统中创建一个文件或目录时,操作系统会为其分配一个空闲的inode,并将文件的元数据信息写入该inode中。因此,inode在文件系统中起着非常重要的作用,它充当了文件和目录的索引,使操作系统能够快速定位和管理文件数据。

为了能解释清楚 inode 我们先简单了解一下文件系统:

 

详细分析:

Linux ext2 文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block 。一个 block 的大小是由格式化的时候确定的,并且不可以更改。例如 mke2fs -b 选项可以设定block 大小为 1024 2048 4096 字节。而上图中启动块( Boot Block )的大小是确定的,

 Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子。

超级块( Super Block ):存放文件系统本身的结构信息。记录的信息主要有: bolck inode 的总量,未使用的block inode 的数量,一个 block inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了。
GDT Group Descriptor Table :块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下。
块位图( Block Bitmap ): Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用。
inode位图( inode Bitmap ):每个 bit 表示一个 inode 是否空闲可用。
i节点表(inode Table) : 存放文件属性 如 文件大小,所有者,最近修改时间等。
数据区(Data blocks):存放文件内容。
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过 touch 一个新文件来看看如何工作。
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc

 为了说明问题,我们将上图简化:

创建一个新文件主要有一下4个操作:

1. 存储属性
内核先找到一个空闲的 i 节点(这里是 263466 )。内核把文件信息记录到其中。
2. 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块: 300,500 800 。将内核缓冲区的第一块数据复制到300 ,下一块复制到 500 ,以此类推。
3. 记录分配情况
文件内容按顺序 300,500,800 存放。内核在 inode 上的磁盘分布区记录了上述块列表。
4. 添加文件名到目录
新的文件名 abc linux 如何在当前的目录中记录这个文件?内核将入口( 263466 abc )添加到目录文件。文件名和inode 之间的对应关系将文件名和文件的内容及属性连接起来。

理解硬链接

我们看到,真正找到磁盘上文件的并不是文件名,而是 inode 。 其实在 linux 中可以让多个文件名对应于同一个inode。 [root@localhost linux]# touch abc [root@localhost linux]# ln abc def [root@localhost linux]# ls -1i
abc def 263466 abc 263466 def
abc和 def 的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数, inode
263466 的硬连接数为 2
我们在删除文件时干了两件事情: 1. 在目录中将对应的记录删除, 2. 将硬连接数 -1 ,如果为 0 ,则将对应的磁盘释放。
软链接
硬链接是通过 inode 引用另外一个文件,软链接是通过名字引用另外一个文件,在 shell 中的做法
263563 -rw-r--r--. 2 root root 0 9 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9 15 17:53 abc.s -> abc
263563 -rw-r--r--. 2 root root 0 9 15 17:45 def

acm
下面解释一下文件的三个时间:
Access 最后访问时间
Modify 文件内容最后修改时间
Change 属性最后修改时间

动态库和静态库

静态库与动态库
静态库( .a ):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库( .so ):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
测试程序
 /add.h/
 #ifndef __ADD_H__
 #define __ADD_H__ 
 int add(int a, int b); 
 #endif // __ADD_H__
 /add.c/
 #include "add.h"
 int add(int a, int b)
 {
 return a + b;
 }
 /sub.h/
 #ifndef __SUB_H__
 #define __SUB_H__ 
 int sub(int a, int b); 
 #endif // __SUB_H__
 /add.c/
 #include "add.h"
 int sub(int a, int b)
 {
 return a - b;
 }
 ///main.c
 #include <stdio.h>
 #include "add.h"
 #include "sub.h"
 
 int main( void )
 {
 int a = 10;
 int b = 20;
 printf("add(10, 20)=%d\n", a, b, add(a, b));
 a = 100;
 b = 20;
 printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
 }

生成静态库

[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o 
ar是gnu归档工具,rc表示(replace and create)
查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a 
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出静态库中的文件
v:verbose 详细信息
[root@localhost linux]# gcc main.c -L. -lmymath
-L 指定库路径
-l 指定库名
测试目标文件生成后,静态库删掉,程序照样可以运行.

库搜索路径

从左到右搜索 -L 指定的目录。
由环境变量指定的目录 ( LIBRARY_PATH
由系统指定的目录
/usr/lib
/usr/local/lib

生成动态库

shared: 表示生成共享库格式
fPIC :产生位置无关码 (position independent code)
库名规则: libxxx.so
示例: [root@localhost linux]# gcc -fPIC -c sub.c add.c
[root@localhost linux]# gcc -shared -o libmymath.so *.o
[root@localhost linux]# ls add.c add.h add.o libmymath.so
main.c sub.c sub.h sub.o

使用动态库

编译选项
l :链接动态库,只要库名即可 ( 去掉 lib 以及版本号 )
L :链接库所在的路径
示例: gcc main.o -o main –L. -lhello

运行动态库

1 、拷贝 .so 文件到系统共享库路径下 , 一般指 /usr/lib
2 、更改 LD_LIBRARY_PATH
[root@localhost linux]# export LD_LIBRARY_PATH=.
 [root@localhost linux]# gcc main.c -lmymath
 [root@localhost linux]# ./a.out
 add(10, 20)=30
 sub(100, 20)=80
3 ldconfig 配置 /etc/ld.so.conf.d/ ldconfig 更新
 [root@localhost linux]# cat /etc/ld.so.conf.d/bit.conf 
 /root/tools/linux
 [root@localhost linux]# ldconfig

使用外部库

系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses 库)
#include <math.h>
#include <stdio.h>
int main(void)
{
 double x = pow(2.0, 3.0);
 printf("The cubed is %f\n", x);
 return 0;
}
gcc -Wall calc.c -o calc -lm
-lm 表示要链接 libm.so 或者 libm.a 库文件
库文件名称和引入库的名称
如: libc.so -> c 库,去掉前缀 lib ,去掉后缀 .so,.a
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值