Linux系统编程——基于文件描述符的文件操作(2)

时间:2021-07-19 21:57:55
  • 文件描述符的复制

  • MMAP文件映射

  • ftruncate修改文件大小

文件描述符的复制

​ 系统调用函数dup和dup2可以实现文件描述符的复制,经常用来重定向进程的stdin(0), stdout(1), stderr(2)。

dup返回新的文件描述符(没有使用的文件描述符的最小编号)。这个新的描述符是旧文件描述符

的拷贝。这意味着两个描述符共享同一个数据结构。

​ dup2允许调用者用一个有效描述符(oldfd)和目标描述符(newfd),函数成功返回时,目标描述符将变成旧描述符的复制品,此时两个文件描述符现在都指向同一个文件,并且是函数第一个参数(也就是oldfd)指向的文件。

注意:文件描述符的复制是指用另外一个文件描述符指向同一个打开的文件,它完全不同于直接给文件描述符变量赋值!

int fd2=fd; //赋值,当释放掉一个,另一个已经不能操作了

函数原型:

#include <uistd.h>

int dup(int oldfd)

int dup2(int oldfd, int newfd)

先看一个简单的例子:

#include <func.h>

//dup是引用计数原理
int main(int argc, char *argv[])
{
    ARGS_CHECK(argc, 2);
    int fd, fd1;
    fd = open(argv[1], O_RDWR);
    ERROR_CHECK(fd, -1, "open");
    printf("fd = %d\n",fd);
    fd1 = dup(fd);
    printf("fd1 = %d\n",fd1);
    char buf[128] = {0};
    int ret;
    ret = read(fd, buf, 5);
    printf("ret = %d, buf = %s\n", ret, buf);
    close(fd);
    memset(buf, 0, sizeof(buf));    
    ret = read(fd1, buf, 5);
    printf("ret = %d, buf = %s\n", ret, buf);
    close(fd1);
    return 0;
}

Linux系统编程——基于文件描述符的文件操作(2)

从上述代码段中可以看出fd和fd1指向同一个文件,并不是两个相同的文件,因为每个文件都有各自的ptr偏移指针,从打印结果中看出,第一次read后读出了“hello”,第二次read后读出了“world”。第一次close(fd)以后文件并没有关闭。

PS:内核的文件打开引用计数为0时,文件正式关闭!

再来看个重定向 stdout 例子:

#include <func.h>

int main(int argc,char* argv[])
{
    ARGS_CHECK(argc,2);
    int fd,fd1;
    fd=open(argv[1],O_RDWR);
    ERROR_CHECK(fd,-1,"open");
    printf("\n");
    close(1);
    fd1=dup(fd);
    printf("fd1=%d\n",fd1);
    close(fd);//关闭对应文件
    printf("you can't see me\n");
    return 0;
}

Linux系统编程——基于文件描述符的文件操作(2)

该程序首先打开了一个文件,返回一个文件描述符,因为默认的就打开了0,1,2表示标准输入,标准输出,标准错误输出。而用close(1),则表示关闭标准输出,此时这个文件描述符就空着了。后面又用dup,此时dup(fd),则会复制一个文件描述符到当前未打开的最小描述符,此时这个描述符为1,即fd1 =1,后面关闭fd自身,然后在用标准输出的时候,发现标准输出重定向到你指定的文件了。那么printf所输出的内容也就直接输出到文件了。

而打开两个文件的情况和文件描述符的复制又是不同的:

#include <func.h>

int main(int argc,char* argv[])
{
    ARGS_CHECK(argc,2);
    int fd,fd1;
    fd=open(argv[1],O_RDWR);
    ERROR_CHECK(fd,-1,"open");
    fd1=open(argv[1],O_RDWR);
    char buf[128]={0};
    int ret;
    ret=read(fd,buf,5);
    ERROR_CHECK(ret,-1,"read");
    printf("ret=%d,buf=%s\n",ret,buf);
    memset(buf,0,sizeof(buf));
    ret=read(fd1,buf,5);
    printf("ret=%d,buf=%s\n",ret,buf);
    return 0;
}

Linux系统编程——基于文件描述符的文件操作(2)

首先每个进程中文件描述符相互分离,即进程1中文件描述符从0、1、2开始,进程2中也从0、1、2开始,两者没有联系,其次打开两次,fd=3 , fd1=4,说明分别有两个指针指向文件地址空间(区别于dup,dup是共享同一个文件指针ptr)。于是就可以读出两个hellw。

dup2(int fdold,int fdnew)也是进行描述符的复制,只不过采用此种复制,新的描述符由用户用参数fdnew显示指定,而不是象dup一样由内核帮你选定(内核选定的是随机的)。对于dup2,如果fdnew已经指向一个已经打开的文件,内核会首先关闭掉fdnew所指向的原来的文件。此时再针对于fdnew文件描述符操作的文件,则采用的是fdold的文件描述符。如果成功dup2的返回值于fdnew相同,否则为-1.

#include <func.h>

int main(int argc,char* argv[])
{
    ARGS_CHECK(argc,2);
    int fd,fd1;
    fd=open(argv[1],O_RDWR);
    ERROR_CHECK(fd,-1,"open");
    printf("\n");
    dup2(STDOUT_FILENO,20);
    fd1=dup2(fd,STDOUT_FILENO);
    printf("fd1=%d\n",fd1);
    close(fd);
    printf("you can't see me\n");
    dup2(20,1);
    printf("you can see me\n");
    return 0;
}

第一个dup2的作用使STDOUT_FILENO定位为20,返回值是第二个参数。而printf是依靠标准输出符打印的,此时标准输出符已经偏移,于是第一条打印看不见,第二条再修改回1后打印才能看见。

MMAP文件映射

头文件 <sys/mman.h>

  • 函数原型

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

int munmap(void* start,size_t length);

  • 参数说明:

start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。

length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理

prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起

PROT_EXEC //页内容可以被执行

PROT_READ //页内容可以被读取

PROT_WRITE //页可以被写入

PROT_NONE //页不可访问

flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体

(一般用这个)MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。

fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。

off_t offset:被映射对象内容的起点。 4K对齐

  • 系统调用

    ​ mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

      注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或System V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

      1、mmap()系统调用形式如下:
      void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
      参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。这里不再详细介绍mmap()的参数,读者可参考mmap()手册页获得进一步的信息。

      2、系统调用mmap()用于共享内存的两种方式:
      (1)使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:
      fd=open(name, flag, mode);
      if(fd<0)
      ...
      ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。
      (2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
      对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可

      3、系统调用munmap()
      int munmap( void * addr, size_t len )
      该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

      4、系统调用msync()
      int msync ( void * addr , size_t len, int flags)
      一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

举例:

#include <func.h>

int main(int argc,char* argv[])
{
    ARGS_CHECK(argc,2);
    int fd;
    fd=open(argv[1],O_RDWR);
    ERROR_CHECK(fd,-1,"open");
    printf("fd=%d\n",fd);
    char *p;
    struct stat buf;    //为了获取文件长度
    fstat(fd,&buf);     //对于已经打开的文件用fstat
    p=(char*)mmap(NULL,buf.st_size,PROT_READ|PROT_WRITE,MAP_SHARED ,fd,0);
    ERROR_CHECK(p,-1,"mmap");
    strcpy(p,"HELLO");
    munmap(p,buf.st_size);
    return 0;
}

mmap比read和write效率更高,原因是read和write是通过系统调用,先将数据存储到内存的内核区再从内核区搬移到堆栈区域,而mmap通过DMA机制直接将数据拷贝到堆区,被称为“零拷贝”(无内核态到用户态的拷贝)

ftruncate修改文件大小
#include <func.h>

int main(int argc,char* argv[])
{
    ARGS_CHECK(argc,2);
    int fd;
    fd=open(argv[1],O_RDWR|O_CREAT,0666);
    ERROR_CHECK(fd,-1,"open");
    printf("fd=%d\n",fd);
    ftruncate(fd,4096);
    char *p;
    struct stat buf;
    fstat(fd,&buf);
    p=(char*)mmap(NULL,buf.st_size,PROT_READ|PROT_WRITE,MAP_SHARED ,fd,0);
    ERROR_CHECK(p,(char*)-1,"mmap");
    memcpy(p,"HELLO",5);
    munmap(p,buf.st_size);
    return 0;
}