IO【1】(函数库及文件IO)

490 阅读12分钟

“这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

函数库

  1. main.c

  2. add.c :函数的实现 ---->add---》程序员的知识产权

  3. add.h :函数的声明---》提供给用户使用

函数库是什么?

函数库是为了保护程序员的知识产权或者说为了对代码进行加密操作,然后将函数的实现封装成一个不可执行的二进制文件,可以进行函数的调用。被称为函数库。

函数库的分类:

分为两大类: 1.静态库 2.动态库

函数库的格式:

静态库: 以lib 开头 + 库名 + .a 为后缀

例如:libadd.a

动态库: 以lib开头 + 库名 + .so 为后缀

例如: libadd.so

静态库

静态库指的是在链接阶段,将我的库文件和我的目标二进制文件编译到一起,形成一个整体,生成可执行的二进制文件

  1. 生成的可执行文件较大

  2. 生成可执行文件后不需要依赖源文件当调用函数时,不需要去外部寻找函数的实现,所以相对来说调用速度是快的。

  3. 后续程序更新时,需要重新编译一次!

    图片.png

静态库的制作

主要步骤:

  1. 第一步:新建四个目录

    bin  include  lib  src
    //项目开发就可以把include lib给别人了
    
  2. 第二步:

    1. 在src目录下vim -O main.c add.c
    2. 在include目录下vim add.h
  3. 第三步:在bin目录下生成一个库函数需要的二进制文件

    1. gcc -c add.c -o ../bin/add.o -I ../include/
  4. 第四步:在lib目录下生成库函数需要的二进制文件生成的库文件

    1. ar -crs ../lib/libadd.a add.o
  5. 第五步:链接库文件:

    1. gcc main.c -I ../include/ -L ../lib/ -ladd

注意:

  • -I :指定头文件的路径
  • -L :指定库文件的路径
  • -l :指定要链接的库文件名(去掉前后缀) (小写的l)

静态库详细过程:

  1. 第一步:新建四个目录:

    linux@ubuntu:~/demo/test/LIB$ ls
    bin  include  lib  src
    
  2. 第二步:

    在src目录下vim -O main.c add.c

    //main.c
    #include <stdio.h>
    #include "add.h"
    
    int main(int argc, char const *argv[])
    {
        int a = 100;
        int b = 200;
        printf("add = %d\n",add(a,b));
    
        return 0;
    }
    
    //add.c
    #include <stdio.h>
    #include "add.h"
    
    int add(int a,int b)
    {
        return a+b;
    }
    

    在include目录下vim add.h

    //add.h
    #ifndef __ADD_H__
    #define __ADD_H__
    
    int add(int a,int b);
    
    #endif
    

    这样完成后我们可以先编译一下,看看代码是否存在问题

    这样会显示,没有头文件

    linux@ubuntu:~/demo/test/LIB/src$ gcc *.c
    add.c:2:10: fatal error: add.h: 没有那个文件或目录
     #include "add.h"
              ^~~~~~~
    compilation terminated.
    main.c:2:10: fatal error: add.h: 没有那个文件或目录
     #include "add.h"
              ^~~~~~~
    compilation terminated.
    

    加上-I ../include就可以了

    linux@ubuntu:~/demo/test/LIB/src$ gcc *.c -I ../include/
    linux@ubuntu:~/demo/test/LIB/src$ ls
    add.c  a.out  main.c
    linux@ubuntu:~/demo/test/LIB/src$ ./a.out 
    add = 300
    
  3. 第三步:在bin目录下生成一个库函数需要的二进制文件

    同样,没链接头文件

    linux@ubuntu:~/demo/test/LIB/src$ gcc -c add.c -o ../bin/add.o
    add.c:2:10: fatal error: add.h: 没有那个文件或目录
     #include "add.h"
              ^~~~~~~
    compilation terminated.
    

    同样,加上-I ../include就可以了

    linux@ubuntu:~/demo/test/LIB/src$ gcc -c add.c -o ../bin/add.o -I ../include/
    linux@ubuntu:~/demo/test/LIB/src$ cd ../bin/
    linux@ubuntu:~/demo/test/LIB/bin$ ls
    add.o
    
  4. 第四步:在lib目录下生成库函数需要的二进制文件生成的库文件

    注意:在指定目录下生成只需类似这样就可以了../lib/libadd.a

    linux@ubuntu:~/demo/test/LIB/bin$ ar -crs ../lib/libadd.a add.o 
    linux@ubuntu:~/demo/test/LIB/bin$ cd ../lib/
    linux@ubuntu:~/demo/test/LIB/lib$ ls
    libadd.a
    
  5. 链接库文件:

    linux@ubuntu:~/demo/test/LIB/src$ gcc main.c -I ../include/ -L ../lib/ -ladd
    linux@ubuntu:~/demo/test/LIB/src$ ls
    add.c  a.out  main.c
    
  6. 最后可以验证一下:

    linux@ubuntu:~/demo/test/LIB/src$ ./a.out 
    add = 300
    

    都放在一个目录下,只要gcc main.c 就行,不需要gcc main.c add.c

动态库

动态库指的是在链接阶段,将库文件和目标二进制文件编译成一个可执行文件,不是形成一个整体,而是在可执行文件中生成一个库函数表单,指定哪一个函数链接的是哪一库。当函数调用时,会去先查找这个库函数表,然后找到要链接的库,然后去外部调用函数。

  1. 相对静态库来说,调用速度要慢一点动态库编译成的二级制文件相比静态库来说体积小一点

  2. 动态库可以被共享

  3. 动态库在后续软件更新方面更加快捷,无需重新编译

    图片.png

制作动态库

主要步骤:

  1. 第一步:新建四个目录

    1. bin include lib src
  2. 第二部:在bin目录下生成一个库文件的二进制不可执行文件

    1. -fPIC :生成与位置无关的代码
    2. gcc -c -fPIC add.c -o ../bin/add.o -I ../include/
  3. 第三步:开始制作动态库,在lib目录下生成库函数需要的二进制文件生成的库文件。

    1. gcc -shared add.o -o ../lib/libadd.so
  4. 第四步:开始链接

    1. gcc main.c -I ../include/ -L ../lib/ -ladd -o ../bin/a.out
  5. 第五步:修改库的环境变量路径

    1. 方法一:把libadd.so放在/usr/bin//bin/

    2. 方法二:修改库的环境变量路径 : LD_LIBRARY_PATH

      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/linux/demo/test/LIB/lib

动态库详细过程:

第一步:新建四个目录

bin  include  lib  src

第二部:在bin目录下生成一个库文件的二进制不可执行文件

📣注意:-fPIC :生成与位置无关的代码

linux@ubuntu:~/demo/test/LIB/src$ gcc -c -fPIC add.c -o ../bin/add.o -I ../include/
linux@ubuntu:~/demo/test/LIB/src$ cd ../bin/
linux@ubuntu:~/demo/test/LIB/bin$ ls
add.o

第三步:开始制作动态库,在lib目录下生成库函数需要的二进制文件生成的库文件

linux@ubuntu:~/demo/test/LIB/bin$ gcc -shared add.o -o ../lib/libadd.so
linux@ubuntu:~/demo/test/LIB/bin$ cd ../lib/
linux@ubuntu:~/demo/test/LIB/lib$ ls
libadd.so//绿色的

第四步:开始链接

linux@ubuntu:~/demo/test/LIB/src$ gcc main.c -I ../include/ -L ../lib/ -ladd -o ../bin/a.out
linux@ubuntu:~/demo/test/LIB/src$ cd ../bin/
linux@ubuntu:~/demo/test/LIB/bin$ ls
add.o  a.out

链接完成后,试着运行一下./a.out

linux@ubuntu:~/demo/test/LIB/bin$ ./a.out 
./a.out: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory
//会发现无法打开目标文件

查看一下库文件的依赖,发现libadd.so => not found

附加linux命令:ldd + 可执行文件名 功能:将文件的链接库显示出来

linux@ubuntu:~/demo/test/LIB/bin$ ldd a.out 
	linux-vdso.so.1 (0x00007fffe59f2000)
	libadd.so => not found//看这里,没有路径
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe753582000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fe753b75000)

接下来有两种路径设置方法

第五步:修改库的环境变量路径

方法一:把libadd.so放在/usr/bin//bin/

linux@ubuntu:~/demo/test/LIB/bin$ sudo cp ../lib/libadd.so /usr/bin/  
//用户的

linux@ubuntu:~/demo/test/LIB/bin$ sudo cp ../lib/libadd.so /bin/
//操作系统的
//但是在工作中你很可能没有sudo权限的
linux@ubuntu:~/demo/test/LIB/bin$ sudo rm /usr/lib/libadd.so
//如果使用下面的操作就执行上面的命令,注意要仔细不能删除错了,因为那里面有很多的重要的.so文件!!!
//这样直接-L就行

方法二:(重点)修改库的环境变量路径 : LD_LIBRARY_PATH

//可以先打印一下路径看一看
linux@ubuntu:~/demo/test/LIB/bin$ env
//指定可执行文件的路径
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
//打印看一下$LD_LIBRARY_PATH
linux@ubuntu:~/demo/test/LIB/bin$ echo $LD_LIBRARY_PATH

linux@ubuntu:~/demo/test/LIB/bin$
//什么都没有,因为指定的那俩个路径是不允许修改的,看都不给看,但是可以往里面添加
//修改库的环境变量路径
linux@ubuntu:~/demo/test/LIB/bin$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/linux/demo/test/LIB/lib

//再次查看
linux@ubuntu:~/demo/test/LIB/bin$ echo $LD_LIBRARY_PATH
:/home/linux/demo/test/LIB/lib

第六步:运行一下./a.out

linux@ubuntu:~/demo/test/LIB/bin$ ls
add.o  a.out
linux@ubuntu:~/demo/test/LIB/bin$ ./a.out 
add = 300  

📣注意:如果指定的库文件夹中有同名的静态库和动态库,优先链接动态库。

动态库的注意事项

  1. 动态库是有固定路径的:

    1. /usr/lib 下
    2. /lib 下
    3. 在使用时需要将库文件放入这两个问价夹中的任意一个即可
  2. 修改库的环境变量路径 : LD_LIBRARY_PATH

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/linux/class/iO/dc21071/day1/01_lib/lib

  3. 在后续再有.c文件要使用该库的函数时,直接调用并且链接就可以了。

    gcc main.c -I ../include/ -L ../lib/ -ladd -o ../bin/a.out

静态库和动态库的区别

IO

学习IO的前提

  1. 为什么学习IO

    ​ 嵌入式:由软件层来控制硬件嵌入式需要对内存进行操作,和对内核进行操作, 来完成操作硬件。

  2. IO的内容都有什么

    ​ 主要学习已经提供好的API函数接口

  3. linux 下一起皆是文件

  4. linux 是一个多用户多任务的操作系统---》进程

  5. 因为IO进程在应用层和内核层之间交互,所以说学习时定义过于抽象

  6. linux操作系统把文件分为两大类

    ​ 二进制文件 --》a.out --》按照计算机的逻辑去存储的

    ​ 文本文件 --》.c 或者 .txt文件 --》按照程序员的逻辑去存储的 底层存储是一样的,逻辑存储上是不一样

IO是什么

IO指的是: input output 指的是对内存的输入和输出

  • 内存:本身速度快,数据掉电就丢失,太贵了
  • 外存:本身速度较慢,数据永久保存,掉电不丢失,外存本身便宜。

如何使用IO

需要借助概念: linux下一切皆是文件。

所以说对文件的操作:打开文件----读写文件---关闭文件

IO分为两大类

1.文件IO :是由操作系统提供的API接口,也被称为系统调用。每个操作系统的IO-api接口都是不一样的。也就造成了你的linux下的可执行文件如果调用了文件IO的话,是不能够移植到别的平台的

因为文件IO的接口是由操作系统提供,不同操作系统接口是不一致的。

2.标准IO:stdio.h --是由标准c库 ANSI C库提供的API接口,标准IO实在文件IO基础上封装出来的。

文件IO接口

  1. open
  2. read
  3. write
  4. close
  5. lseek

注意:

  • windows 函数接口 _open、 _read、 _write、 _close

  • ios 函数接口: Open、Read、 Write、 Close

man手册的使用

man :在线函数查询接口

现在主要学习2和3。

图片.png

**errno **

errno是一个全局变量,存在与#include 中,用于标注错误信息的,那么当函数报错返回时,会将这个全局变量重新根据错误信息表中的对应的号码设置一下。

在return之前就根据表给全局变量赋值了,比如现在要判断一个文件打开是否存在,如果存在就继续往下执行,如果不存在就return -1了,打印出对应的错误。

图片.png

图片.png

文件

Linux操作系统是基于文件概念的。文件是以字符序列构成的信息载体。根据这一点,可以把I/0设备当作文件来处理。因此,与磁盘上的普通文件进行交互所用的同一系统调用可以直接用于I/0设备。这样大大简化了系统对不同设备的处理,提高了效率。

Linux中的文件主要分为7种:普通文件、目录文件、符号链接文件、管道文件、套接字文件和设备文件。

  1. - : 普通文件 (白色)⬜
  2. d : 目录 (蓝色)🟦
  3. l : (link) 链接文件 分为软链接和硬链接(类似于window的快捷方式)(浅蓝色)
  4. c : 字符设备文件(/dev/input/mouse0鼠标)event1 键盘 usdo hexdump mouse0b
  5. : 块设备文件 (硬盘 /dev/sda)
  6. p : 管道文件 (进程间通讯)
  7. s (socket) : 套接字文件(网络通讯相关的文件)**

文件描述符

文件描述符(File descriptor,简称fd):为内核区分和引用特定的文件。对于linux而言,所有对设备和文件的操作都是通过文件描述符来进行的。

文件描述符是一个文件标志,是一个非负整数,值是小的。是一个索引值,并指向在内核中进行的每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;读写文件时,需要把文件描述符作为参数传递给相应的函数。

基于文件描述符的I/O操作时Linux中的常用操作之一

图片.png

📣注意

./a.out在刚运行时。内核会自动为他打开3个设备文件,返回三个文件描述符

1.标准输入 ---- 0

2.标准输出 ---- 1

3.标准错误输出 -- 2

API接口的使用

附加:函数的三要素:功能、参数、返回值

1.open

  1. 头文件:

    1. #include <sys/types.h>
    2. #include <sys/stat.h>
    3. #include <fcntl.h>
  2. 原型:

    1. int open(const char *pathname, int flags);
    2. int open(const char *pathname, int flags, mode_t mode);
  3. 功能: 打开一个文件

  4. 参数:

    1. pathname :路径及名称 例如: "1.txt" //以后参数是char* 类型一般直接填写字符串
    2. flags :打开文件的方式

    必须包括: O_RDONLY:只读权限打开文件 O_WRONLY:只写权限打开文件 O_RDWR :读写权限打开文件

  5. 附加参数:(以或的形式去附加的) O_APPEND :以追加写的方式打开文件 O_CREAT :如果文件存在则打开文件, 如果文件不存在则创建文件,如果使用了O_CREAT这个参数,需要使用open的第三个参数,给与权限是一个8进制的整型。以0开头。

    例如:0664 O_EXCL : 如果文件存在则报错返回

                O_NONBLOCK :以非阻塞方式打开一个文件  
    
                O_TRUNC :以清空的方式打开文件
                
    
  6. 返回值:

    1. 成功返回一个文件描述符
    2. 失败返回-1

注意:使用0666创建文件为什么出来的是0664呢 ? 是由于操作系统有一个叫做文件掩码的机制,将其他用户权限的可写权限抹掉 使用umask查看文件掩码 修改文件掩码 umask + 想要修改的权限值。

代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc, const char *argv[])
{
	int fd = open("./1.txt",O_RDWR);
	if(-1 == fd)
	{
		perror("open");
		printf("errno = %d\n",errno);
		return -1;
	}
	perror("open1");

	return 0;
}

linux@ubuntu:~/demo/test/IO/test$ ./a.out 
open1: Success

2.perror

头文件: #include <stdio.h>

原型:void perror(const char *str);

功能:以固定的格式打印错误信息

参数:s :判断的函数名字 格式:”str : 当前最新的错误信息“

返回值:无

3.read

  1. 头文件:#include <unistd.h>

  2. 原型:ssize_t read(int fd, void *buf, size_t count);

  3. 功能:对一个文件进行读取

  4. 参数:

    1. fd :目标文件描述符 ---》对谁进行读取
    2. buf :存放读到的数据 ---》读到的数据存放的地址
    3. count:读多少
  5. 返回值:

    1. 成功:返回读到的字节个数
    2. 失败:-1 读到文件末尾返回0

注意:但是最后一次read时如果文件剩余字节不够read的conut,读到文件 末尾时返回已经读到的字节个数,再一次读取时返回0

代码:

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

int main(int argc, const char *argv[])
{
	int fd = open("1.txt",O_RDONLY);
	if(-1 == fd)
	{
		perror("open");
		return -1;
	}
	
	char buf[123] = {0};
	if(-1 == read(fd,buf,123))
	{
		perror("read");
		return -1;
	}

	printf("\n读取到的值为buf = %s\n",buf);

	if(-1 == close(fd))
	{
		perror("close");
		return -1;
	}

	return 0;
}
linux@ubuntu:~/demo/test/IO/test$ ./a.out 

读取到的值为buf = hello world

4.write

  1. 头文件: #include <unistd.h>

  2. 原型:

    1. ssize_t write(int fd, const void *buf, size_t count);
    2. typedef long ssize_t
    3. typedef unsigned long size_t
  3. 参数:

    1. fd:目标文件描述符--》要对哪个文件进行操作
    2. buf:要写入的数据的地址 --》要写入什么东西
    3. count :写入数据的大小 --》写多少,以字节为单位
  4. 返回值:

    1. 成功: 写入的字节个数
    2. 失败:-1

代码:

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

int main(int argc, const char *argv[])
{
	int fd = open("./1.txt",O_WRONLY);
	if(-1 == fd)
	{
		perror("open");
		return -1;
	}
	char buf[123] = "hello world";

	if(-1 == write(fd,buf,strlen(buf)))
	{
		perror("write");
		return -1;
	}
	
	return 0;
}

5.close

  1. 头文件:#include <unistd.h>

  2. 原型:int close(int fd);

  3. 参数:fd 要关闭的文件描述符

  4. 返回值:

    1. 成功返回 0
    2. 失败返回 -1

注意:一个可执行程序最多能打开1024个文件描述符,这个值可以同过内核去修改

练习:先对一个文件进行写入hello world 然后在从这个文件中将hello world读出来,打印到终端上

结果:写入数据时正常的,读取时未读到数据,这是因为读写指针的相互影响导致

6.lseek

  1. 头文件:

    1. #include <sys/types.h>
    2. #include <unistd.h>
  2. 原型:

    1. off_t lseek(int fd, off_t offset, int whence);
    2. typedef long off_t
  3. 功能:操作读写指针,对读写指针进行偏移

  4. 参数:

    1. fd :目标文件描述符 :----》对哪个文件操作

    2. offset:如何偏移,偏移多少

      1. 如果该数为负数,代表这向前进行偏移,

      2. 如果偏移出了文件的开头,会报错返回

      3. 如果该数为正数,如果偏移出了文件,代表着向后进行偏移件的末尾,会扩大文件,用'\0'来做填充。

        那么此类的文件被称为 --空洞文件--

      📣注意:如果偏移后没有对其进行任何写入操作,内核认为该偏移无效,不会扩大文件大小。

    3. whence:基准位置-----根据哪一个位置进行偏移

      1. SEEK_SET:根据文件开头进行偏移
      2. SEEK_CUR:根据用户当前位置进行偏移
      3. SEEK_END:根据文件末尾进行偏移
  5. 返回值:

    1. 成功: 返回偏移的字节个数(根据文件开头来定)
    2. 失败: (off_t)-1
//获取文件大小利用偏移量
int len = lseek(fd,0,SEEK_END);             

代码:

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

int main(int argc, const char *argv[])
{
    //打开一个要写入和读取的文件
    int fd = open(argv[1],O_RDWR|O_CREAT,0666);
    if (-1 == fd )
    {
        perror("open");
        return -1;
    }
    //开始进行写入数据
    
    if (-1 == write(fd,"hello world",11))
    {
        perror("write");
        return -1;
    }
    //开始读写指针偏移

    if (-1 == lseek(fd,-6,SEEK_END))
    {
        perror("lseek");
        return -1;
    }
    //开始读取数据
    char buf[123] = {0}; //存放读取的数据
    if ( -1 == read(fd,buf,sizeof(buf)))
    {
       perror("read");
       return -1;
    }
    printf("buf = %s \n",buf);

    if( -1 == close (fd))
    {
        perror("close");
        return -1;

    }    
    return 0;
}

作业:自己去实现cp命令 mycp(保证可以去拷贝二进制文件)

读取时循环读取,写入时循环写入,使用argv参数

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define SIZE 123

int main(int argc, const char *argv[])
{
    //判断我的命令行传参是否有问题
    if (3 != argc)
    {
        printf("输入参数错误,例如:./a.out + 源文件 + 目标文件\n");
        return -1;
    }
    //打开要复制的文件以只读方式,打开要粘贴的文件,以写入并且创建并且清空
    int fd = open(argv[1],O_RDONLY);
    if (-1 == fd)
    {
        perror("open");
        return -1;
    }
    int fd1 = open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0664);
    if (-1 == fd1 )
    {
        perror("open1");
        return -1;
    }
    //开始读取--》边读边写
    while(1)
    {
        //先读
        char buf[SIZE] = {0};//作为数据的中转站
        ssize_t ret = read(fd,buf,sizeof(buf));
        if(-1 == ret)
        {
            perror("read");
            return -1;
        }
        if (0 == ret)
        {
            printf("读取到文件的末尾,退出while循环\n");
            break;
        }
        //写入
        if(-1 == write(fd1,buf,ret))
        {
            perror("write");
            return -1;
        }  
    }
    //读取写入完成后
    close(fd);
    close(fd1);
    return 0;
}

文件IO和标准IO的区别

《面试题》:文件IO和标准IO各有所长。文件IO属于系统调用,由操作系统系统提供。速度快, 但是频繁调用文件IO会降低内核的工作效率。因为每个操作系统PAI接口是不一样的 所以造成了移植性差的问题。标准IO是由标准C库所提供,是在文件IO的基础上封装 出来的API接口,可移植性得到了提升。并且在文件IO的基础上封装了一片缓冲区。目的是为了存放不着急的数据,从而降低文件IO的调用次数,提高内核的工作效率 所以说会根据具体数据情况来使用这两种IO模型。