一. C库函数
之前,我们有用fopen,fclose,fread,fwrite来对文件进行打开,关闭,读取,写入操作。上述几个函数都是C标准库中的函数。在调用以上函数时,都会涉及到一个FILE*指针,那它到底是什么呢?
这里,首先要知道C库函数是对系统调用的一层封装,也就是说,在执行C库函数时,这些函数调用了系统提供的接口函数。上述四个函数调用的系统接口分别是open,close,read,write。所以,先来认识下这些系统调用接口函数。
二. 系统调用接口函数
1. open函数
函数原型:
int open(const char* pathname,int flags);
int open(const char* pathname,int flags,mode_t mode);
头文件:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
参数说明:
pathname:要打开的文件名(实际是文件路径)
flags:选项如下,传入多个选项时,用“或”运算连接
O_RDONLY:以只读方式打开文件
O_WRONLY:以只写方式打开文件
O_RDWR:以读写方式打开
注意:以上三个选项必须指定一个且只能指定一个
O_CREAT:创建文件,创建时要设置文件权限mode
O_APPEND:追加写
注意:以上两个选项必须和上述三个选项之一共同使用
返回值:
若文件打开成功,返回新打开文件的文件描述符
若失败,返回-1。
2. close函数
函数原型:
int clode(int fd);//头文件<unistd.h>
参数fd:上述open返回的文件描述符
返回值:成功返回0,失败返回-1。
3. read函数
函数原型:
ssize_t read(int fd,void* buf,size_t count);//头文件<unistd.h>
函数功能:从文件中读取内容写入buf中
参数:
fd:读取的文件的文件描述符
buf:从文件中读取内容写入buf中
count:设置一次读取的字节数
返回值:
读取成功:返回实际读取的字节数(大于等于0)
读取失败:返回-1
4. write函数
函数原型:
ssize_t write(int fd,const void* buf,size_t count);//头文件:<unistd.h>
函数功能:将buf中的内容写入文件中,其中count表示一次写入的字节数
返回值:成功,返回实际写入的字节数,失败,返回-1。
接下来,用系统调用接口来实现上述C库函数的功能:
(1)写入文件
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
umask(0);//消除掩码对权限造成的影响
int fd = open("myfile1",O_WRONLY|O_CREAT,0644);//创建文件myfile1,并以只写的方式打开
if(fd < 0)//文件打开失败
{
perror("open");
return 1;
}
const char* msg = "hello world\n";
int count = 4;
while(count--)
{
write(fd,msg,strlen(msg));
}
close(fd);
return 0;
(2)读取文件
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
int fd = open("myfile1",O_RDONLY);//以只读的方式打开文件
if(fd < 0)//文件打开失败
{
perror("open error");
exit(1);
}
char buf[1024];
const char* msg = "hello world\n";
while(1)
{
ssize_t s = read(fd,buf,strlen(msg));//从文件中读取内容到buf中
if(s > 0)
{
buf[s] = 0;
printf("%s",buf);
sleep(1);
}
else if(s == 0)//文件到达结尾
{
break;
}
}
close(fd);
return 0;
}
运行可发现,上述两段代码可实现与C库函数相同的功能。
同时,上述两段代码中都出现了一个整数fd,我们发现他跟FILE*的作用很相似,都是通过他们来对文件进行操作。那么fd代表的是什么,它与FILE*的作用又是什么?
三. 文件描述符fd
上面的代码中发现fd是一个整数,那该整数代表的含义是什么?fd实际是一个指针数组的下标,如下图:
最右侧的是i_node节点表,在整个系统中只有一张。该表可视为file结构体数组。结构体中存放文件属性等相关信息,如文件大小等。
往右是文件表,在整个系统中也只有一张。同样可被视为一个结构体数组,一个文件对应一个数组元素该结构体中有很多字段,主要为上图中的三个。其中,文件状态标志表示文件的打开方式如读或写等。当前文件偏移量表示当前文件的读写位置。v节点指针指向对应文件的file结构体。
再往右是文件描述符表files_struct,每个进程有一张该表。该表是一个指针数组,每个数组元素为一files*类型的指针,指向文件表中的一个数组元素。而该数组元素的下标即为文件描述符。
最左边的是一个进程的PCB,它也是一个结构体,在该结构体中有一成员*files,它是一指针变量,指向文件描述符表即files_struct。
这样,当在一个进程中打开一个文件时,操作系统在内存中的i_node表中创建一个file结构体来描述该文件。同时,在文件表中创建一个数组元素,存放文件状态标志和文件偏移量以及节点指针,并使指针指向刚刚创建的file结构体。再在files_struct中找到未使用的最小下标,在该位置存放一个file*指针,指向文件表中对应的结构体数组元素。而进程PCB中的指针files再指向files_struct,这样就可以使进程和文件相关联起来了。所以,只要拿着文件描述符,就可以找到对应的文件。
那么文件描述符是如何分配的呢?
(1)首先,Linux进程会默认打开3个文件,分别是标准输入,标准输出,标准错误。对应的设备分别是:键盘,显示器,显示器。所以,会默认占用最小的三个数组下标:0,1,2。如:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
char buf[1024];
ssize_t s = read(0,buf,sizeof(buf));//从标准输入即键盘上读取内容到buf中
if(s > 0)
{
write(1,buf,strlen(buf));//将buf的内容写到标准输出即显示器上
write(2,buf,strlen(buf));//将buf的内容写到标准错误即显示器上
}
return 0;
}
运行结果:
[admin@localhost basic_IO.c]$ gcc stdout.c
[admin@localhost basic_IO.c]$ ./a.out
nsxkla//输入的内容
nsxkla//输出的内容
nsxkla//输出的内容
(2)在files_struct数组中,找到当前未被使用的最小的一个下标,作为新文件的文件描述符。如:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int fd = open("myfile",O_RDONLY);
if(fd < 0)
{
perror("open error");
exit(1);
}
printf("fp:%d\n",fd);
return 0;
}
运行结果:
[admin@localhost basic_IO.c]$ gcc stdout.c
[admin@localhost basic_IO.c]$ ./a.out
fp:3
因为一个进程会默认打开三个文件,所以新文件从3开始。
当在上述代码开始时加入一条语句:
close(0);
此时,因为关闭了标准输入文件,所以最小下标为0,所以新文件的fd即为0。
知道了fd所表示的意义,那FILE与fd是什么关系呢?
C库函数封装了系统调用函数,通过fd可以找到文件,而通过FILE也可以找到文件。所以,有理由相信,FILE能找到文件,必然是通过fd。而我们知道FILE是一个结构体。所以,FILE结构体内部必然封装了fd。
四. 重定向
输出重定向:本应输出到显示器上的内容,输出到了其他文件上。如:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
int main()
{
close(1);
int fd = open("myfile",O_WRONLY|O_CREAT,0644);
if(fd < 0)
{
perror("open error");
exit(1);
}
printf("fd:%d\n",fd);
fflush(stdout);
close(fd);
return 0;
}
在上述代码中,printf本应该向显示器中输出内容,但实际是向myfile文件中输出了内容。这是为什么?
因为,printf是C库函数,一般往stdout中输出,而stdout是FILE结构体类型的,该结构体中封装了文件描述符1,原本下标1中的内容是显示器文件的相关地址。而在上述代码中,因为关闭了1所代表的文件,所以新创建的文件myfile的文件描述符即为1,所以,下标1中的内容即为myfile的相关指针。
因此,printf找到stdout,stdout找到下标1,1找到myfile,所以向myfile中输出了内容,进而完成了输出重定向。
五. 结构体FILE
上述已经知道了FILE是一结构体,该结构体中封装了文件描述符fd,所以才能对文件进行操作。为进一步了解FILE,看下面的例子:
#include<stdio.h>
#include<string.h>
int main()
{
const char* msg0 = "printf\n";
const char* msg1 = "fwrite\n";
const char* msg2 = "write\n";
const char* msg3= "fprintf\n";
printf("%s",msg0);
fwrite(msg1,1,strlen(msg1),stdout);
write(1,msg2,strlen(msg2));
fprintf(stdout,"%s",msg3);
fork();
return 0;
}
运行结果如下:
[admin@localhost basic_IO.c]$ gcc buffer.c
[admin@localhost basic_IO.c]$ ./a.out
printf
fwrite
write
fprintf
当将输出结果重定向到另一文件时,文件中的内容如下:
write
printf
fwrite
fprintf
printf
fwrite
fprintf
这是什么原因呢,为什么往显示器上输出会正常输出,而往普通文件上输出会出现这样的结果呢?
这里,就涉及到了缓冲区的问题。缓冲区分为以下几种:
(1)无缓冲
(2)行缓冲,遇到换行符\n则清空缓冲区。一般用于往显示器上写入内容。
(3)全缓冲,等到程序结束,才会清空缓冲区。一般用于往普通文件中写入内容。
那上面的问题就可以解释了。
(1)当往显示器上输出时,因为是行缓冲,所以加上\n会立即清空缓冲区。所以,在fork之前内容已经写入完成,fork之后,缓冲区为空,所以,子进程并没有做任何事情。所以会输出上述结果。
(2)当往普通文件上写入时,因为是全缓冲,所以执行写入操作时,会将内容先写入缓冲区(write暂时不考虑)。而fork之后会创建子进程,操作系统会父进程的缓冲区内容原样拷贝给子进程。所以程序结束时,会清空父,子进程的缓冲区,所以会导致每条语句多输出一条语句(write暂不考虑)。
那为什么只有write只输出一条,而其他语句都输出两条呢?
上述已经知道除了write是系统调用之外,其余的函数都是C库函数。而C库函数是对系统调用的封装。这就可以说明,C库函数是自带缓冲区的,而系统调用不带缓冲区。由此,便可以说明缓冲区是C库函数提供的,而系统调用没有缓冲区。关于缓冲区的相关信息,被描述在结构体FILE中。可以通过在Linux下查看文件/usr/include/libio.h来获取相应的缓冲区的信息和封装的fd。
六. 静态库/动态库
当我们运行一份代码时,一般都会用到一个函数printf。那该函数的实现代码在哪里存放呢?为什么只写了一个函数名和引入了头文件就可以使用它呢?这是因为,在程序编译链接时会去找到这些库函数所在的位置来供我们使用。根据链接方式的不同,可以分为动态链接和静态链接,因此可以分为动态库和静态库。
1. 静态库
程序在进行编译链接时将静态库的代码连接到可执行文件中。生成可执行文件后将不再使用静态库。
2. 动态库
程序在编译链接时,先找到动态库所在的位置,并将该位置信息包含在可执行文件中。在程序进行运行时在根据该位置信息找到动态库,然后去调用它。所以动态库在程序运行时采取链接。
3. 区别
程序在静态连接时,将静态库的内容拷贝至可执行文件中,所以会导致可执行文件的代码量增多。但生成可执行文件之后不再依赖静态库。所以速度会较快。
在进行动态链接时,只是拷贝动态库的位置信息至可执行文件中,所以代码量会相对较少。而多个文件可以共享动态库,这样便可以节省内存和磁盘空间。但是,每次使用时,都要去调用动态库,所以速度会相对较慢。
4. 生成动/静态库
比如,在主函数中要用到两个函数add和sub对数字进行加减操作。
(1)首先编写这两个函数代码:
头文件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__
文件sub.c中的内容:
#include"sub.h"
int sub(int a,int b)
{
return a-b;
}
主函数代码:
#include<stdio.h>
#include"add.h"
#include"sub.h"
int main()
{
int a = 10;
int b = 20;
int ret1 = add(a,b);
printf("a+b = %d\n",ret1);
int ret2 = sub(a,b);
printf("a-b = %d\n",ret2);
return 0;
}
(2)生成静态库
首先,将两方法函数生成.o文件:
[admin@localhost lib]$ ls
add.c add.h lib_a lib_so main.c sub.c sub.h
[admin@localhost lib]$ gcc -c add.c
[admin@localhost lib]$ gcc -c sub.c
[admin@localhost lib]$ ls
add.c add.h add.o lib_a lib_so main.c sub.c sub.h sub.o
然后,将.o文件打包归档静态库:
[admin@localhost lib]$ ar -rc -o libmymath.a *.o //ar是归档工具,libmymath.a是静态库名
[admin@localhost lib]$ ls
add.c add.h add.o lib_a libmymath.a lib_so main.c sub.c sub.h sub.o
注意:静态库名格式均为:lib+自己定义的库名+后缀.a
最后,在main.c文件编译链接时链接该静态库。
1)根据-L指定静态库的路径
如果静态库与main.c,头文件在同一目录下,输入如下命令:
[admin@localhost lib_a]$ ls
add.h libmymath.a main.c sub.h
[admin@localhost lib_a]$ gcc main.c -o main -L. -lmymath
[admin@localhost lib_a]$ ls
add.h libmymath.a main main.c sub.h
[admin@localhost lib_a]$ ./main
a+b = 30
a-b = -10
其中,-L指定静态库所在的路径,-l指定静态库的库名,该库名是去掉前缀lib和后缀.a之后的名字。
2)由环境变量LIBRARY_PATH指定静态库所在的路径(若静态库在main.c的上一级目录下)
[admin@localhost lib_a]$ ls
add.h main.c sub.h
[admin@localhost lib_a]$ export LIBRARY_PATH=..
[admin@localhost lib_a]$ gcc main.c -o main -lmymath
[admin@localhost lib_a]$ ls
add.h main main.c sub.h
[admin@localhost lib_a]$ ./main
a+b = 30
a-b = -10
此时,只需要指定静态库名即可。
3)将静态库拷贝至系统库文件所在的目录:/usr/lib或/usr/local/lib下,再编译链接main.c即可(此时,还需要指定库名)。
(3)生成动态库
首先,将两个方法函数编译链接生成.o文件,与静态库不同的是,此时要加-fPIC选项生成与位置无关码:
[admin@localhost lib]$ ls
add.c add.h lib_a libmymath.a lib_so main.c sub.c sub.h
[admin@localhost lib]$ gcc -c add.c -fPIC
[admin@localhost lib]$ gcc -c sub.c -fPIC
[admin@localhost lib]$ ls
add.c add.h add.o lib_a libmymath.a lib_so main.c sub.c sub.h sub.o
然后,将.o文件进行打包归档,因为要共享动态库,所以,要加-shared选项:
[admin@localhost lib]$ ls
add.c add.h add.o lib_a libmymath.a lib_so main.c sub.c sub.h sub.o
[admin@localhost lib]$ gcc -shared -o libmymath.so *.o
[admin@localhost lib]$ ls
add.c add.h add.o lib_a libmymath.a libmymath.so lib_so main.c sub.c sub.h sub.o
动态库命名与静态库相同,只是后缀为.so。
再编译链接main.c生成可执行程序,此时也需要指定动态库所在的路径及库名(若动态库在当前目录下):
[admin@localhost lib_so]$ ls
add.h libmymath.so main.c sub.h
[admin@localhost lib_so]$ gcc -c main.c -o main -L. -lmymath
[admin@localhost lib_so]$ ls
add.h libmymath.so main main.c sub.h
最后,运行可执行程序。因为运行程序时要进行动态链接找到动态库,然后去调用它,所以也要指定动态库所在的路径,有如下方法:
1)由环境变量LD_LIBRARY_PATH指定动态库所在的路径:
[admin@localhost lib_so]$ ls
add.h libmymath.so main main.c sub.h
[admin@localhost lib_so]$ export LD_LIBRARY_PATH=.
[admin@localhost lib_so]$ ./main
a+b = 30
a-b = -10
环境变量指定的动态库路径是为方便可执行程序运行时找到。
2)将动态库拷贝到系统指定的库目录/usr/lib下,在运行即可。
3)配置/etc/ld.so.conf.d/
先看下该目录下的内容:
[admin@localhost lib_so]$ cd /etc/ld.so.conf.d
[admin@localhost ld.so.conf.d]$ ls
kernel-2.6.32-431.el6.i686.conf mysql-i386.conf vmware-tools-libraries.conf xulrunner-32.conf
当运行系统提供的程序时,会找到这些.conf文件,按照文件中的路径信息找到要找的程序。所以,只需要将动态库的路径保存在.conf文件中即可。这里创建一个.conf文件去存放动态库的路径:
mkdir mymath.conf
在该文件中存放动态库的路径
保存退出后,使用ldconfig命令更新/ld.so.conf.d目录。
完成上述操作后,即可运行可执行程序。