系统调用接口,文件描述符,重定向,FILE,动/静态库

27 篇文章 2 订阅

 一. 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目录。

        完成上述操作后,即可运行可执行程序。










  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值