程序员成长之旅——基础IO
open/read/write/close等文件相关系统调用接口
open
注释解析:
(1)函数声明:用来打开一个已经存在的文件或者创建一个普通文件
(2)参数解释:
pathname:要打开或者创建的目标文件
flags:打开文件的时候,可以传入多个参数选项,用下面的一个或多个进行“或”运算,构成flags;
参数:O_RDONLY----只读打开、
O_WRONLY----只写打开、
O_RDWR----读、写打开;
这三个变量只能指定一个
O_CREAT----若文件不存在,则创建它。需要使用mode(文件权限标志)选项,来指明权限
O_APPEND----追加写,要配合上面三个变量中的一个
(3)返回值:成功返回新打开文件的描述符,失败则返回-1
read
//函数原型
ssize_t read(int fd,void *buf,size_t count);
注释:
(1)函数声明:是从fd所描述的打开文件中读取buf所缓冲区的n个字节
(2)参数说明
fd:文件描述符,用来指向要操作的文件的文件结构体
buf:一块内存空间
count:请求读取的字节数,读上来的数缓冲区buf中,同时文件的当前读写位置向后移。
ssize_t:有符号整型
(3)返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调用read之前已到达文件末尾,则这次read返回0。
注意:读操作从文件的当前位移量处开始,在成功返回之前,该位移量增加实际读的字数。
write
//函数原型
ssize_t write(int fd,const void *buf,size_t count);
注释:
(1)函数声明:将 buf所指向的缓冲区的 n字节写入 fd 所描述的打开文件中
(2)参数说明
fd:文件指针
buf:写入的数据保存在缓冲区buf中,同时文件的当前读写位置向后移
count:请求写入的字节数
(3)返回值:成功返回写入的字节数,出错返回-1并设置errno写常规文件时,write的返回值通常等于请求写的字节数count,而向终端或者网络写则不一定。
close
注释:
(1)函数声明:关闭指定文件
(2)参数说明:fd—文件描述符,用来指向要操作的文件的文件结构体
(3)返回值:若成功返回0,出错返回-1;
注意:关闭一个文件时也释放该进程加在该文件上的所有记录锁,当一个进程终止时,它所有的打开文件都由内核自动关闭。
纵向对比fd与FILE结构体
文件描述符fd
我们都知道在Linux下一切皆文件。当然设备也不例外,如果要对某个设备进行操作,就不得不打开此设备文件,打开文件就会获得该文件的文件描述符fd( file discriptor), 它就是一个很小的整数,每个进程在PCB中都有一个指针*files,指向一张表files_struct,表中包含一个指针数组,每个元素都是指向打开文件的指针。文件描述符就是这个表的索引,即为该数组的下标。
文件描述符分配规则:在 file_struct数组中,找到当前没有被使用的最小下标,作为一个新的文件描述符。
注意:一般情况下,file_struct数组下标的 0,1,2 分别被标准输入(键盘),标准输出(显示器),标准错误(显示器)占用,只有当调用close()函数,关闭掉某个下标 ,fd才会是0或1,或2.若没有被关闭的,fd会是3。 其次,由于数组下标没有负数,所以fd也不会是负数。
所以,通过上面的介绍,可以知道,fd的本质其实就是一个表示数组下标的小整数。
FILE结构体
struct FILE
{
char *_ptr;//文件输入的下一个位置
int _cnt;//当前缓冲区的相对位置
char *_base;//指基础位置(文件的起始位置)
int _flag;//文件标志
int _file;//文件的有效性验证
int _charbuf;//检查缓冲区状况,如果缓冲区则不读取
int _bufsiz;//文件的大小
char *_tmpfname;//临时文件名
};
FILE*文件指针
文件指针指向进程用户区中一个被叫做FILE结构的结构数据。FILE结构包括一个缓冲区和一个文件描述符 。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。
通常,任何程序运行起来之后都会默认的打开三个标准输入流(stdin:键盘),标准输出流(stdout:显示器),标准错误流(stderr:显示器)。
简单来说,fd是一个一个表示数组下标的小整数。FILE为一个结构体,FILE* 为文件指针,指向文件。
文件描述符和文件指针的区别
fd是一个表示数组下标的整数,在打开文件的过程中,起到一个索引的作用,进程通过PCB中的文件描述符找到所指向的文件指针file*。
open:文件描述符的操作(如:open)返回的是一个文件描述符(int fd),内核会在每个进程空间中维护一个文件描述符表,此表中的文件描述符来引用。
fopen:流(如:fopen)返回的是一个文件指针(即指向FILE结构体的指针),FILE结构是包含有文件描述符的,fopen可以看做是open(fd直接操作的系统调用)的封装,它的优点是带有I/O缓存。
之前编写的自主shell进行修改,使其支持输入/输出/追加重定向
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
//建立一个子进程(fork)
//在子进程中获取命令行
//在子进程中解析命令行,并判断是那种重定向替换相应的文件描述符
//替换子进程(execvp)
//父进程等待子进程退出(wait)
int main()
{
char buf[1024] = {0};
while(1){
//打印提示符
printf("[lpf@MyShell]$");
fflush(stdout);
//获取命令行
fgets(buf,sizeof(buf) - 1,stdin);
buf[strlen(buf) - 1] = '\0';
//解析命令
char *argv[8] = {NULL};
argv[0] = strtok(buf," ");
int argc = 0;
while(argv[argc]){
argv[++argc] = strtok(NULL," ");
}
//创建子进程
pid_t id = fork();
if(id < 0){
perror("fork");
}else if(id == 0){
//子进程
int i = 0;
for(i = 0;i < argc;++i){
if(strcmp(argv[i],">") == 0){
int fd = open(argv[i+1],O_WRONLY|O_CREAT);
dup2(fd,1);
argv[i] = NULL;
}else if(strcmp(argv[i],">>") == 0){
int fd = open(argv[i+1],O_WRONLY|O_CREAT|O_APPEND);
dup2(fd,1);
argv[i] = NULL;
}else if(strcmp(argv[i],"<") == 0){
int fd = open(argv[i+1],O_RDONLY,0644);
dup2(fd,1);
argv[i] = NULL;
}
}
//子进程程序替换
execvp(argv[0],argv);
perror("execvp");
}else{
//父进程等待子进程
wait(NULL);
}
}
return 0;
}
效果演示
1.普通的
2.>>
3.>
4.<
编写简单的add/sub/mul/div函数,并打包成动/静态库,并分别使用
静态库
.h头文件
#ifdef _ADD_H_
#define _ADD_H_
#include<stdio.h>
int myadd(int a,int b);
#endif
#ifdef _DIV_H_
#define _DIV_H_
#include<stdio.h>
int mydiv(int a,int b);
#endif
#ifdef _MUL_H_
#define _MUL_H_
#include<stdio.h>
int mymul(int a,int b);
#endif
#ifdef _SUB_H_
#define _SUB_H_
#include<stdio.h>
int mysub(int a,int b);
#endif
.c源文件
#include"add.h"
int myadd(int a,int b)
{
return a + b;
}
#include"div.h"
int mydiv(int a,int b)
{
return a / b;
}
#include"mul.h"
int mymul(int a,int b)
{
return a * b;
}
#include"sub.h"
int mysub(int a,int b)
{
return a - b;
}
静态库的生成Makefile
这是将上面的.h和.c源文件打包生成了静态库lib
然后编写cal.c并在Makefile输入下面这个链接找到库文件就可以运行了
-I后面找的是.h文件
-L找的是静态库所在的目录
-l找的是库名
静态库名是去掉后缀.a和lib这个剩下的才是库名
ldd命令可以查看可执行程序依赖的第三方库
我们发现,并没有依赖静态库,所以我们知道静态库是在链接的时候向可执行程序中拷贝一份,所以我们并不依赖静态库。
#include<stdio.h>
#include"add.h"
#include"sub.h"
#include"mul.h"
#include"div.h"
int main()
{
printf("PLease enter: ");
int x,y,z;
scanf("%d %d",&x,&y);
z = myadd(x,y);
printf("%d\n",z);
z = mysub(x,y);
printf("%d\n",z);
z = mymul(x,y);
printf("%d\n",z);
z = mydiv(x,y);
printf("%d\n",z);
return 0;
}
上面的运行结果是:
动态库
shared----表示生成共享库格式
FPIC----产生位置无关码
库名规则----libxxx.so
打包成动态库先需要更改一下Makefile如下图所示
然后我们make一下,在ldd一下,我们会发现要依赖的库知道了,但是程序找不到库
这时候我们只需要加入这句命令即可
ldd cal发现可以找到了
运行也是可以完成
综上所述,我们可以总结一下动静态库的区别也就是优缺点:
静态库
- 代码装载速度快,执行速度略比动态链接库快;
- 只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地域等问题。
- 使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;
动态库
- 更加节省内存并减少页面交换;
- DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
- 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
- 适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
- 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;速度比静态链接慢。当某个模块更新后,如果新模块与旧的模块不兼容,那么那些需要该模块才能运行的软件,统统撕掉。这在早期Windows中很常见。