前言
一、open/read/write/close等文件相关系统调用接口
谈到系统调用接口,首先回顾一下系统调用的定义以及库函数与系统调用的区别:
- 系统调用接口:在开发角度操作系统对外会表现一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。如下:
open: 打开文件或设备
read: 从打开的文件或设备中读取数据
write: 向打开的文件或设备中写入数据
close: 关闭文件或设备
ioctl: 把控制信息传递给设备驱动文件
系统调用是通向操作系统本身的接口,是面向底层硬件的。通过系统调用,可以使得用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互,是操作系统留给应用程序的一个接口。
用户进程需要发生系统调用时,内核将调用内核相关函数来实现(如sys_read(),sys_write(),sys_fork())。用户程序不能直接调用这些函数,这些函数运行在内核态,CPU 通过软中断切换到内核态开始执行内核系统调用函数。用户态–>系统调用–>内核态–>返回用户态
实际上使用系统调用会影响系统的性能,在执行调用时的从用户态切换到内核态,再返回用户态会有系统开销。为了减少开销,因此需要减少系统调用的次数,并且让每次系统调用尽可能的完成多的任务。硬件也会限制对底层系统调用一次所能写的数据块的大小。为了给设备和文件提供更高层的接口,Linux系统提供了一系列的标准函数库。使用标准库函数,可以高效的写任意长度的数据块,库函数在数据满足数据块长度要求时安排执行底层系统调用。
一般地,操作系统为了考虑实现的难度和管理的方便,它只提供一少部分的系统调用,这些系统调用一般都是由C和汇编混合编写实现的,其接口用C来定义,而具体的实现则是汇编,这样的好处就是执行效率高,而且,极大的方便了上层调用。
- 库函数:系统调用在使用上,功能基础基础,对用户的要求也比较高,所以,有心的开发者可以对部分系统调用接口进行适度封装,就形成了库。有了库,就很有利于更上层用户或者开发者进行二次开发。
库函数(Library function)是把函数放到库里,供别人使用的一种方式。.方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。一般放在.lib文件中。库函数调用则是面向应用开发的,库函数可分为两类,一类是C语言标准规定的库函数,一类是编译器特定的库函数。(由于版权原因,库函数的源代码一般是不可见的,但在头文件中你可以看到它对外的接口)。
glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库,即运行时库。这些基本函数都是被标准化了的,而且这些函数通常都是用汇编直接实现的。glibc 为程序员提供丰富的 API(Application Programming Interface),我们经常说到的POSIX(Portable Operating System Interface of Unix)是针对API的标准,即针对API的函数名,返回值,参数类型等。POSIX兼容也就指定这些接口函数兼容,但是并不管API具体如何实现。
随着系统提供的这些库函数把系统调用进行封装或者组合,可以实现更多的功能,这样的库函数能够实现一些对内核来说比较复杂的操作。比如,read()函数根据参数,直接就能读文件,而背后隐藏的比如文件在硬盘的哪个磁道,哪个扇区,加载到内存的哪个位置等等这些操作,程序员是不必关心的,这些操作里面自然也包含了系统调用。而对于第三方的库,它其实和系统库一样,只是它直接利用系统调用的可能性要小一些,而是利用系统提供的API接口来实现功能(API的接口是开放的)。部分Libc库中的函数的功能的实现还是借助了系统掉调用,比如printf的实现最终还是调用了write这样的系统调用;而另一些则不会使用系统调用,比如strlen, strcat, memcpy等。
- 区别
(1)库函数是语言或应用程序的一部分,而系统调用是内核提供给应用程序的接口,属于系统的一部分
(2)库函数在用户地址空间执行,系统调用是在内核地址空间执行,库函数运行时间属于用户时间,系统调用属于系统时间,库函数开销较小,系统调用开销较大
(3)库函数是有缓冲的,系统调用是无缓冲的
(4)系统调用依赖于平台,库函数并不依赖
了解了系统调用,接下来一起看看几个简单系统调用接口:
1. 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:追加写
//返回值:
成功:新打开的文件描述符
失败:-1
mode_t理解
:文件权限标志也可以使用加权数字表示,这组数字被称为umask变量,它的类型是mode_t,是一个无符号八进制数。
umask变量表示方法:
注意
:open函数具体使用哪个,和具体应用场景相关,如果目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限。否则,使用两个参数的open。
2. close详解
//头文件
#include<unistd.h>
//函数原型
int close(int fd);
//返回值
若成功返回0,出错返回-1;
注意
:关闭一个文件时也释放该进程加在该文件上的所有记录锁,当一个进程终止时,它所有的打开文件都由内核自动关闭。
3. read 详解
//头文件
#include<unistd.h>
//函数原型
ssize_t read(int fd,void *buf,size_t count);
//参数说明:
fd:文件描述符,用来指向要操作的文件的文件结构体
buf:一块内存空间
count:请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。
ssize_t:有符号整型
//返回值
成功返回读取的字节数,
出错返回-1并设置errno,如果在调用read之前已到达文件末尾,则这次read返回0。
有很多种情况可使实际读到的字节数少于要求读字节数:
(1)读普通文件时,在读到要求字节数之前已到达了文件尾端,如,若在到达文件尾端之前还有30字节,而要求读100个字节,则read返回30,下一次在调用read时,它将返回0.
(2)当从终端设备读时,通常一次最多读一行。
(3)当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。
(4)某些面向记录的设备,如磁带一次最多返回一个记录。
注意
:读操作从文件的当前位移量处开始,在成功返回之前,该位移量增加实际读的字数。
- 参考代码
int main()
{
int fd = open("myfile", O_RDONLY);
if (fd<0)
{
perror("use open");
exit(1);
}
char buf[1024];
const char *msg = "hello\n";
while (1)
{
ssize_t s = read(fd, buf, strlen(msg));
if (s>0)
{
buf[s] = 0;
printf("%s", buf);
}
else
{
break;
}
}
close(fd);
return 0;
}
4. write详解
//头文件
#include<unistd.h>
//函数原型
ssize_t write(int fd,const void *buf,size_t count);
//返回值
成功返回写入的字节数,
出错返回-1并设置errno写常规文件时,write的返回值通常等于请求写的字节数count,而向终端或者
- 参考代码
int main()
{
int fd = open("myfile", O_WRONLY | O_CREAT, 0664);
if (fd <= 0)
{
perror("use open");
exit(1);
}
const char* msg = "hello,xikeda\n";
int count = 5;
while (count--){
write(fd, msg, strlen(msg));
}
close(fd);
return 0;
}
二、纵向对比fd与FILE结构体
三、编写简易版shell,支持输入/输出/追加重定向功能
四、编写简单的add/sub/mul/div函数,并打包成动/静态库,并分别使用。
1.什么是静态库和动态库
- 静态库
静态库是程序在编译链接时把库的代码链接到可执行文件中去。程序运行的时候不需要依赖库。它以.a结尾。
- 动态库
动态库是在程序运行起来的时候才去链接动态库的代码,多个程序可以共享使用库的代码。它以.so结尾。
动态函数库与共享函数库语义相同(在linux上叫共享对象库, 文件后缀是.so,windows上叫动态加载函数库, 文件后缀是.dll)
- Linux中命名系统中共享库的规则
最前面使用前缀"lib", 中间是库的名字,后缀是".so", 最后面跟着的是三个数字组成的版本好。x表示主版本号, y表示此版本号, z表示发布版本号
三个版本号含义不同:
主版本号
: 表示库的重大升级, 不同主版本号的库之间是不兼容的, 依赖于旧的主版本号的程序要改动相应的部分,并且重新编译,才可以在新版本的共享库中运行; 或则,系统必须保留就版的共享库,使得那些依赖于旧版共享库的程序能够正常运行。次版本号
: 表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变。在主版本号相同的情况下,高的次版本号的库向后兼容低的此版本号的库。一个依赖于旧的次版本好共享库的程序,可以在新的次版本好共享库中运行,应为新版本中保留了原来所有的接口,并且不改变他们的定义和含义发布版本号
:表示库的一些错误的修正,性能的改进等,并不添加任何新的接口, 也不对接口进行更改。.发布版本号:表示库的一些错误的修正,性能的改进等,并不添加任何新的接口, 也不对接口进行更改。
2.动态库和静态库的优缺点及区别
- 静态库的优点及缺点
优点:
可移植性较强,一旦可执行程序编译成功不需要依赖静态库
缺点
:
每一份程序都需要将静态库的代码链接进去,生成的可执行程序较大,浪费空间
- 动态库的优点及缺点
优点:
在程序运行起来,才链接库,生成的可执行代码较小,节约内存
可以被多个程序共享(虚拟内存机制)
缺点:
可移植性差。一旦库丢失了,所有用到它的程序都会崩溃
- 区别
(1).它们都是对二进制文件(.o)进行打包;
(2).在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中赋值到内存中,这个过程称之为动态链接
;
(3).动态链接的可执行文件只需要包含函数入口地址的一个表,而静态链接需要外部函数的所有机器码;
(4).动态库可以被多个进程共享(采用虚拟内存机制),所以动态链接使得可执行文件更小,节约了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程使用,节省了内存和磁盘空间。而静态库只能在编译阶段将库的代码链接到可执行文件中;
3.生成并使用静态库和动态库
- 参考代码
- 经过编译阶段
只经编译器生成.o文件
- 生成静态库
第一步:
将.o文件通过ar工具打包成静态库
ar -rc libmymath.a sub.o sum.o(-r:打包,-c:创建)
第二步:查看静态库中的目录文件
ar -tv libmymath.a(-t:列出静态库中的文件,-v:详细信息)
第三步:使用静态库
(静态库在当前目录)
gcc main.c -L. -lmymath(-L:指定库路径,-l:指定库名,把lib和后缀删掉)
在实际使用
中,我们会把头文件和静态库
给别人,防止源代码泄露
。我们可以新建一个文件夹
,结构如下。lib下放的是静态库,而include下放的头文件
。
在使用时,需要加上-I + 头文件所在的路劲
、-L+静态库所在路劲
与-l+库名称
即可编译成功。
gcc main.c -L ./staticlib/lib -lmymath -I ./staticlib/include
对于库搜索路径
可以有三种方式:
1.用-L指定库目录路径
2. 用环境变量LIBRARY_PATH指定库目录路径
3. 把库放入系统指定搜索目录/usr/lib或者/usr/local/lib
Makefile
可以这样编写
- 生成动态库
第一步:
产生与位置无关代码
gcc -fPIC -c sub.c sum.c(-fPIC参数会产生与位置无关代码,是为了能够在多个应用程序间共享,.so要求PIC码以达到动态链接的目的,否则,无法实现动态链接。)
第二步:生成动态库
gcc -shared -o libmath.so *.o,加上-shared参数表示生成共享库的格式
以上两步可以合并为一步:gcc -fPIC -shared -o libmath.so *.o
第三步:使用动态库
gcc main.c -L. -lmath ,-l:代表链接动态库,只要去掉lib和后缀即可、-L:链接库所在的路径
第三步这个可以编译通过,但是链接时找不到相应库
,因为库文件在链接(静态库和动态库)和运行(仅限于使用动态库的程序)时被使用
,其搜索路径
是在系统中进行设置的。一般Linux系统把/lib 和 /usr/lib(也可能是/usr/local/lib)三个目录作为默认的库搜索路径
所以使用这两个目录中的库时不需要进行设置路径即可直接使用。对于处于默认库搜索路径之外
的库,需要将库的位置添加到搜索路径之中。所以第三步出现链接错误,所以应该对库路径进行设置。
要想运行动态库,可以通过以下三种方法:
1. 把动态库拷贝到系统默认搜索路径,/usr/lib或者/lib
此种做法会污染标准库,强烈不建议
2. 把动态库的路径加入到环境变量LD_LIBRARY_PATH中
推荐做法
3. ldonfig配置/etc/ld.so.config.d/, ldconfig更新
,此方法难理解,就是进入ld.so.conf.d
新建目录手动添加库路径,推荐第二种,在此放上第三种方法教程 详细教程
同样,我们也可以把头文件和库放在一个目录
中,供别人使用,防止源码泄露
: