目录
一、系统文件接口
1.1 open
int open(const char* pathname,int flags,mode_t mode)
open的第一个参数
open函数的第一个参数是pathname,表示要打开或创建的目标文件
若pathname以路径的方式给出,则需要创建该文件时,在pathname路径下进行创建
若pathname以文件名的方式给出,则需要创建该文件时,默认在当前路径下进行创建
open的第二个参数
open函数的第三个参数
表示创建文件的默认权限
open的返回值
open函数成功调用后返回打开文件的文件描述符,若调用失败则返回-1。
1.2 write
Linux系统接口中使用write函数向文件写入信息
ssize_t write(int fd,const void* buf,size_t count)
将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
若数据写入成功,则返回实际写入数据的字节数。
若数据写入失败,则返回-1。
1.3 read
inux系统接口中使用write函数向文件写入信息
ssize_t read(int fd,void* buf,size_t count)
从文件描述符为fd的文件读取count字节的数据到buf位置当中
若数据读取成功,则返回实际读取数据的字节数
若数据读取失败,则返回-1
1.4 close
使用close函数时传入需要关闭文件的文件描述符(即调用open函数的返回值)即可。若关闭文件成功则返回0;若关闭文件失败则返回-1。
int close(int fd)
二、文件描述符
三、文件描述符的分配规则
现象代码1:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd1 = open("./log1.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
int fd2 = open("./log2.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
int fd3 = open("./log3.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
int fd4 = open("./log4.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
int fd5 = open("./log5.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
printf("fd1: %d\n",fd1);
printf("fd2: %d\n",fd2);
printf("fd3: %d\n",fd3);
printf("fd4: %d\n",fd4);
printf("fd5: %d\n",fd5);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
close(fd5);
return 0;
}
现象结果1:
现象代码2:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(0);
close(2);
int fd1 = open("./log1.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
int fd2 = open("./log2.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
int fd3 = open("./log3.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
int fd4 = open("./log4.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
int fd5 = open("./log5.txt",O_RDWR | O_CREAT | O_TRUNC,0666);
printf("fd1: %d\n",fd1);
printf("fd2: %d\n",fd2);
printf("fd3: %d\n",fd3);
printf("fd4: %d\n",fd4);
printf("fd5: %d\n",fd5);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
close(fd5);
return 0;
}
现象结果2:
总结:文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的
四、重定向
重定向包含了输入重定向、追加重定向、输出重定向。
其中的原理本质上都一样。
4.1输出重定向的原理
输出重定向的本质是,将本应该输出到A文件的数据输出到B文件中。
若想让本应该输出到"显示器文件"的数据输出到log.txt文件当中,可以在打开log.txt文件之前将文件描述符为1的文件关闭(即将“显示器文件”关闭)。当我们后续打开log.txt文件时所分配到的文件描述符就是1。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);
int fd = open("./log.txt",O_RDWR | O_CREAT | O_TRUNC, 0666);
if(fd < 0){
perror("opern error:");
return 1;
}
printf("hello world\n");
fflush(stdout);//为什么需要刷新?阅读后续章节《缓冲区》
close(fd);
return 0;
}
4.2dup2函数的系统调用
在Linux环境下还可以使用dup2()系统调用来实现重定向。dup2()本质上是通过fd_array数组中地址元素的拷贝完成重定向的。
#include <unistd.h>
int dup2(int oldfd, int newfd);
函数功能: 将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中
函数返回值: 若调用成功则返回newfd,否则返回-1
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
五、缓冲区
5.1代码及现象
5.2原理解释
5.3C语言FILE
因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。
在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名。
typedef struct _IO_FILE FILE;
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
};
六、文件系统
6.1磁盘的介绍
磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备,与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。
磁盘中最基本的单元是扇区–512字节/4kb,可以把整个磁盘由无数个扇区组成。
要把数据存到磁盘中,首先要解决的定位扇区问题:
那一面(根据磁头Header)?哪个磁道Cylinder?哪个扇区Sector。
通过上面三个来定位扇区的方式又称为CHS寻址。
6.1磁盘的分区管理
计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group).
如何理解创建一个空文件?
通过编列inode位图的方式,找到一个空间的inode
在inode Tbale中找到对应的inode,并将文件的属性信息填充到inode结构中。
将该文件的文件名与inode添加到目录文件的数据块中。
如何理解对文件写入信息?
通过文件的inode编号找到对应的inode结构。
通过inode结构找到存储该文件内容的数据块,并将数据写入到数据块
若不存在数据块或申请的数据块已经被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据块区当中找到对应的空闲块,再将数据写入数据块,最后将建立数据块与inode结构的对应关系。
如何理解删除一个文件?
将该文件对应的inode在inode位图当中置为无效。
将该文件申请过的数据块在块位图当中置为无效。
为什么拷贝文件的时候特别慢,而删除文件却特别块?
因为拷贝文件需要创建文件、然后对文件进行写入操作,这个过程需要先申请inode号然后填充文件的属性,再申请数据块,将文件内容写入到数据块最后建立数据块与inode结构的对应关系。
而删除文件只需要把对应的inode号与数据块在位图中设置无效即可。
如何理解目录?
目录也是文件,
目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小,目录的拥有者等。
目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode映射关系。
7、软硬连接
7.1软连接
软连接有独立的inode,也有独立的数据块,数据块的内容是指向的文件的路径。
软链接就类似于Windows操作系统当中的快捷方式。
但是软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了。
7.2硬连接
硬连接文件的inode号与源文件的inode号相同,并且文件的属性也相同。可以认为硬链接是源文件的别名,一个inode有几个文件名该inode的硬链接数就是多少
为什么刚创建的目录的硬链接数是2?
原因:每个目录创建后,该目录下默认会有两个隐含文件.和…,它们分别代表当前目录和上级目录。这个dir与.是一样的。
7.3区别
1.软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
2.软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。
八、Linux动静态库
8.1了解动静态库
一堆源文件和头文件最终变成一个可执行程序需要经历以下四个步骤:
1.预处理: 完成头文件展开、去注释、宏替换、条件编译等,最终形成xxx.i文件
2.编译: 完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成xxx.s文件
3.汇编: 将汇编指令转换成二进制指令,最终形成xxx.o文件
4.链接: 将生成的各个xxx.o文件进行链接,最终形成可执行程序
实际上,所有库本质是一些目标文件(xxx.o)的集合,库的文件当中并不包含主函数而只是包含了大量的方法以供调用,可以认为动静态库本质是可执行程序的"半成品"
- 在Linux当中,以.so为后缀的是动态库,以.a为后缀的是静态库
- 在Windows当中,以.dll为后缀的是动态库,以.lib为后缀的是静态库
通过ldd命令可以查看可执行程序所依赖的库文件
根据上图可以看出来test可执行程序依赖/lib64/lib.so.6,本质上是一个软连接,链接的源文件是/lib64/libc-2.17.so。这个libc-2.17.so就是C的动态库,去掉库的前缀,再去掉后缀,剩下的就是库的名字。
而gcc/g++默认进行的是动态链接,想使用静态链接需添加 -static 选项,且静态链接生成的可执行程序并不依赖其他库文件。
而且 使用静态库的可执行程序的大小 明显大于 使用动态库的可执行程序的大小
8.1.1静态库的特点
静态库是程序在编译链接的时候把库的代码复制到可执行文件当中的,生成的可执行程序在运行的时候将不再需要静态库,因此使用静态库生成的可执行程序的大小一般比较大
- 优点:使用静态库生成可执行程序后,该可执行程序可独自运行,不再依赖库了
- 缺点:使用静态库生成可执行程序会占用大量空间,特别是当有多个静态程序同时加载而这些静态程序使用的都是相同的库,这时在内存当中就会存在大量的重复代码
8.1.2动态库的特点
动态库是程序在运行的时候才去链接相应的动态库代码的,不必将库的代码复制到可执行文件当中,使得可执行程序的大小较使用静态库而言更小,节省磁盘空间。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。操作系统采用虚拟内存机制使得物理内存中的一份动态库被所有要使用该库的进程共用,节省了内存空间。
8.2静态库的打包与使用
8.2.1静态库的打包
第一步:将打包的源文件生成对应的目标文件
func1.o:func1.c
gcc -c func1.c -o func1.o
func2.o:func2.c
gcc -c func2.c -o func2.o
第二步:使用ar命令将所有的目标文件打包成静态库
- -r(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件
- (create):建立静态库文件
libfunc.a:func1.o func2.o
ar -rc libfunc.a func1.o func2.o
此外,我们可以用ar命令的- t 和 -v 选项查看静态库当中的文件。
-t:列出静态库中的文件
-v(verbose):显示详细的信息
第三步:将头文件和生成的静态库组织
mkdir -p my_lib/include
mkdir -p my_lib/lib
cp ./*.h ./my_lib/include
cp ./*.a ./my_lib/lib
makefile完整版
libfunc.a:func1.o func2.o
ar -rc $@ $^
func1.o:func1.c
gcc -c $^ -o $@
func2.o:func2.c
gcc -c $^ -o $@
.PHONY:output
output:
mkdir -p my_lib/include
mkdir -p my_lib/lib
cp ./*.h ./my_lib/include
cp ./*.a ./my_lib/lib
.PHONY:clean
clean:
rm -rf ./my_lib ./*.o ./*.a
8.2.1静态库的使用
方案一:使用选项
-I:指定头文件搜索路径
-L:指定库文件搜索路径
-l:指明需要链接库文件路径下的具体哪一个库
方案二:将头文件和库文件拷贝到系统路径下
sudo cp my_lib/include/* /usr/include/
sudo cp my_lib/libfunc.a /lib64/
实际上拷贝头文件和库文件到系统路径下的过程,就是安装库的过程。但并不推荐将个人编写的头文件和库文件拷贝到系统路径下,会对系统文件造成污染。所以我就不想演示这个过程了。
8.3动态库的打包与使用
8.3.1动态库的打包
第一步:将要打包的源文件生成对应的目标文件
此时需要添加 -fPIC 选项 ,即产生位置无关码
test1.o:test1.c
gcc -fPIC -c test1.c -o test1.o
test2.o:test2.c
gcc -fPIC -c test2.c -o test2.o
-fPIC作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行(通过页表与共享区建立映射关系)。所以共享库被加载时,在内存的位置不是固定的 。
第二步:使用-shared选项将所有目标文件打包为动态库
libtest.so:test1.o test2.o
gcc -shared $^ -o $@
第三步:组织头文件与生成的动态库
mkdir -p my_lib/include
mkdir -p my_lib/lib
cp ./*.h ./my_lib/include
cp ./*.so ./my_lib/lib
makefile完整版
libtest.so:test1.o test2.o
gcc -shared $^ -o $@
test1.o:test1.c
gcc -fPIC -c test1.c -o test1.o
test2.o:test2.c
gcc -fPIC -c test2.c -o test2.o
.PHONY:output
output:
mkdir -p my_lib/include
mkdir -p my_lib/lib
cp ./*.h ./my_lib/include
cp ./*.so ./my_lib/lib
.PHONY:clean
clean:
rm -rf *.so *.o my_lib
8.3.1动态库的打包
第一步:将要打包的源文件生成对应的目标文件
此时需要添加 -fPIC 选项 ,即产生位置无关码
func1.o:func1.c
gcc -fPIC -c $@ -o $^
func2.o:func2.c
gcc -fPIC -c $@ -o $^
-fPIC作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行(通过页表与共享区建立映射关系)。所以共享库被加载时,在内存的位置不是固定的 。
第二步:使用-shared选项将所有目标文件打包为动态库
libtest.so:func1.o func2.o
gcc -shared $^ -o $@
第三步:组织头文件与生成的动态库
mkdir -p my_lib/include
mkdir -p my_lib/lib
cp ./*.h ./my_lib/include
cp ./*.so ./my_lib/lib
makefile完整版
libtest.so:func1.o func2.o
gcc -shared $^ -o $@
func1.o:func1.c
gcc -fPIC -c $@ -o $^
func2.o:func2.c
gcc -fPIC -c $@ -o $^
.PHONY:output
output:
mkdir -p my_lib/include
mkdir -p my_lib/lib
cp ./*.h ./my_lib/include
cp ./*.so ./my_lib/lib
.PHONY:clean
clean:
rm -rf *.so *.o my_lib
8.3.2动态库的使用
接下来使用与静态库相同的方式,用-I选项指定头文件搜索路径,用-L选项指定库文件搜索路径,最后用-l选项指明库的名称。但是可以发现可执行程序貌似找不到这个动态库的位置。