Never give up

glibc memcpy() 源码浅谈

上一篇 / 下一篇  2017-06-14 11:21:27 / 个人分类:C语言

http://blog.csdn.net/yangbodong22011/article/details/53227560
这是一篇很好的文章

这个解释相当经典。

其实我本来只是想搞懂为什么memcpy()函数的参数类型是void *的:

我以为会在memcpy()源码中能找到答案,其实并没有,void *只是在传递参数的时候起了作用,可以让memcpy()接受不同的指针类型,比如char *,double *,struct stu *等等,没错,只是这样,至于memcpy()内部的工作原理,请继续往下看。

没办法,我只好去找找memcpy的源码。代码如下。
备注:glibc-2.8 memcpy.c

#include <string.h>#include <memcopy.h>#include <pagecopy.h>#undef memcpyvoid*memcpy(dstpp, srcpp, len)void*dstpp;constvoid*srcpp;
     size_t len;
{unsignedlongintdstp = (longint) dstpp;unsignedlongintsrcp = (longint) srcpp;/* Copy from the beginning to the end.  *//* If there not too few bytes to copy, use word copy.  */if(len >= OP_T_THRES)//OP_T_THRES=16{/* Copy just a few bytes to make DSTP aligned.  */len -= (-dstp) % OPSIZ;//OPSIZ=sizeof(unsigned long int)=8BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);/* Copy whole pages from SRCP to DSTP by virtual address manipulation,
     as much as possible.  */PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);/* Copy from SRCP to DSTP taking advantage of the known alignment of
     DSTP.  Number of bytes remaining is put in the third argument,
     i.e. in LEN.  This number may vary from machine to machine.  */WORD_COPY_FWD (dstp, srcp, len, len);/* Fall out and copy the tail.  */}/* There are just a few bytes to copy.  Use byte memory operations.  */BYTE_COPY_FWD (dstp, srcp, len);returndstpp;
}
libc_hidden_builtin_def (memcpy)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

整个函数的处理流程如下所示:

1:函数参数

void*memcpy(dstpp, srcpp, len)void*dstpp;constvoid*srcpp;
     size_t len;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

忽略这种传参的形式,传入的三个参数分别是目的地址(void *dstpp)源地址(const void *srcpp)长度(size_t len)

2:地址被转换成unsigned long int保存。

unsignedlongintdstp = (longint) dstpp;unsignedlongintsrcp = (longint) srcpp;
  • 1
  • 2
  • 1
  • 2

8个字节的地址被转换成unsigned long int保存,因为sizeof(unsigned long int) = 8,刚好是8字节,如果还是不能理解把地址转成unsigned long int,看下面的程序:

#include<stdio.h>intmain(intargc,char*argv[])
{inta =256;int*p = &a;unsignedlongintds = (longint)p;printf("%d %d\n",*(int*)ds,sizeof(unsignedlongint));return0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里写图片描述

地址只有一个唯一的ds,上面的程序是将ds转成int*,是因为我们将它按照int对待了,那如果我想以字节char处理它呢,那是不是转成char *就行了,我们用下面这张图先分析下上面代码的情况。
这里写图片描述

a变量的值为256,是一个int类型的,4 个字节,在内存中的分布就像上面所示。我们将地址转成int *,那么接着我就会处理四个字节,如果我将地址转成char *,那我只处理一个字节不就好了。代码变成这样:

#include<stdio.h>intmain(intargc,char*argv[])
{inta =256;int*p = &a;unsignedlongintds = (longint)p;printf("%d %d\n",*(char*)ds,sizeof(unsignedlongint));return0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里写图片描述

果然,输出的值变为了0,就是我们上面图中char *只指向第一个字节。为0。

好了,说了这么多,有什么用呢?

不要着急,我们继续往下走看源码;

3:拷贝数量较小,采用one byte one byte拷贝。

if(len>=OP_T_THRES)//OP_T_THRES=16 {......}
 BYTE_COPY_FWD (dstp, srcp, len);returndstpp;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果len >= OP_T_THRESOP_T_THRES在不同的系统或者平台有不同的值,通常为16或者8),那就按照if中的结构处理,否则执行BYTE_COPY_FWD (dstp, srcp, len)。也就是拷贝的长度如果大于16或者8,我们采用if中的策略,如果小于16或者8,就one byte one byte拷贝。

4:如果拷贝数量较大,采用if中的策略

数量较大势必会牵扯到内存对齐的问题:

/* Copy just a few bytes to make DSTP aligned.  */len-=(-dstp)%OPSIZ;  
BYTE_COPY_FWD (dstp, srcp, (-dstp)%OPSIZ);
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

(-dstp) % OPSIZ这个神奇的式子可以算出来对齐需要移动的字节数,就像下面这样:
这里写图片描述

这一部分比较少的字节仍然采用one byte one byte拷贝。

BYTE_COPY_FWD (dstp, srcp, (-dstp)%OPSIZ);//传的参数为(-dstp) % OPSIZ),就是对齐需要拷贝的字节数
  • 1
  • 1

此时的len就为对齐之后的剩余大小了,之后的处理方式直接按照虚拟内存页的大小来加快拷贝效率。

/* Copy whole pagesfromSRCPtoDSTPbyvirtual address manipulation,asmuchaspossible.  */
    PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
  • 1
  • 2
  • 1
  • 2

如果后面还有剩余部分,还可以采用一个字一个字拷贝:

WORD_COPY_FWD (dstp, srcp,len,len);

 /* Fall outandcopy the tail.  */
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

大家肯定注意到了源码中的三个函数:

  1. BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);
  2. PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
  3. WORD_COPY_FWD (dstp, srcp, len, len);

其中 dstp 和 srcp 都是保存了地址的unsigned long int类型的数。那为什么传入它们竟然能实现在按照不同的类型bytepageword来拷贝呢(忽略有没有page这种类型的细节,我只是想说前面三种类型大小是不同的),那一定在这三个函数中做了类似与我们前面举的例子的转化,我们去找找看,果然,找到一些情况。

memcopy.h

这里写图片描述

转成byte *再去操作,因为是one byte one byte拷贝。

pagecopy.h

这里写图片描述

转成vm_address_t类型去处理。

wordcopy.c

这里写图片描述

其中op_t是一个宏定义,为unsigned long int

5: 总结

说了这么多,我们基本从宏观上缕清了memcpy的流程。

  • 先用unsigned long int保存地址
  • 然后判断需要拷贝的大小,如果小于OP_T_THRES,直接one byte one byte拷贝。
  • 如果大于OP_T_THRES,首先对齐。然后需要对齐的部分按照one byte one byte拷贝。对齐之后的按照one page one page拷贝,最后剩余的还可以按照one word one word拷贝。

至于每种拷贝方式的细节我没有深究下去,主要是能力有限,但是还是学到了memcpy()这种鸡贼的对于地址处理方法,地址只是地址,想具体指向多大范围取决于将它转换成那种类型。看到源码里面有goto语句,我的内心几乎是崩溃的,就先写到这儿吧,欢迎大家评论交流指正~



TAG:

 

评分:0

我来说两句

我的栏目

日历

« 2024-04-28  
 123456
78910111213
14151617181920
21222324252627
282930    

数据统计

  • 访问量: 21535
  • 日志数: 14
  • 建立时间: 2016-10-17
  • 更新时间: 2017-06-28

RSS订阅

Open Toolbar