其实我本来只是想搞懂为什么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;if(len >= OP_T_THRES){len -= (-dstp) % OPSIZ;BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);WORD_COPY_FWD (dstp, srcp, len, len);}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;
忽略这种传参的形式,传入的三个参数分别是目的地址(void *dstpp)
、源地址(const void *srcpp)
、长度(size_t len)
。
2:地址被转换成unsigned long int
保存。
unsignedlongintdstp = (longint) dstpp;unsignedlongintsrcp = (longint) srcpp;
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;
}
地址只有一个唯一的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;
}
果然,输出的值变为了0
,就是我们上面图中char *
只指向第一个字节。为0。
好了,说了这么多,有什么用呢?
不要着急,我们继续往下走看源码;
3:拷贝数量较小,采用one byte one byte
拷贝。
if(len>=OP_T_THRES)......}
BYTE_COPY_FWD (dstp, srcp, len);returndstpp;
}
如果len >= OP_T_THRES
(OP_T_THRES
在不同的系统或者平台有不同的值,通常为16或者8),那就按照if
中的结构处理,否则执行BYTE_COPY_FWD (dstp, srcp, len)
。也就是拷贝的长度如果大于16或者8,我们采用if
中的策略,如果小于16或者8,就one byte one byte
拷贝。
4:如果拷贝数量较大,采用if
中的策略
数量较大势必会牵扯到内存对齐的问题:
len-=(-dstp)%OPSIZ;
BYTE_COPY_FWD (dstp, srcp, (-dstp)%OPSIZ);
(-dstp) % OPSIZ
这个神奇的式子可以算出来对齐需要移动的字节数,就像下面这样:
这一部分比较少的字节仍然采用one byte one byte
拷贝。
BYTE_COPY_FWD (dstp, srcp, (-dstp)%OPSIZ);
此时的len
就为对齐之后的剩余大小了,之后的处理方式直接按照虚拟内存页的大小来加快拷贝效率。
/* Copy whole pagesfromSRCPtoDSTPbyvirtual address manipulation,asmuchaspossible. */
PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
如果后面还有剩余部分,还可以采用一个字一个字拷贝:
WORD_COPY_FWD (dstp, srcp,len,len);
/* Fall outandcopy the tail. */
大家肯定注意到了源码中的三个函数:
- BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);
- PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
- WORD_COPY_FWD (dstp, srcp, len, len);
其中 dstp 和 srcp 都是保存了地址的unsigned long int
类型的数。那为什么传入它们竟然能实现在按照不同的类型byte
,page
,word
来拷贝呢(忽略有没有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
语句,我的内心几乎是崩溃的,就先写到这儿吧,欢迎大家评论交流指正~