调程序真的不能偷懒、想当然,真的,实实在在的体会到。
    做了半年的linux内核编程,前段时间一直在对socket网络通信的内核函数作改动,为了某种科研目的(呵呵,老板一再强调,科研出成果之前,一定不能外泄,所以不具体解释什么目的了)。目标很简单,要让两个进程共享一个socket,并且在读/写的时候对数据进行同步。其实就linux文件系统本身而言,允许两个进程共享文件。其具体方式为两个dentry可以指向一个inode结构,dentry再分别连上file结构再挂向进程的文件描述符表,并在inode结构中增加引用计数就OK了。但是对于socket,虽然作为一种特殊文件,并不提供这种共享方式。
    于是,必须对socket通信所涉及的一些基本数据结构,以及主要操作函数进行改动。为了简单化,我努力将编码限定在Linux内核中的BSD层(socket层),也就是net/socket.c这个C文件,并未触及TCP层,当然,对文件系统相关改动不仅限于此。改写很顺利,sys_socket(), sys_bind(), sys_listen(), sys_accept(), sys_connect()等函数很快完成,无非是添加一些同步点,主进程该做的做,从进程不该做的别做,只是在sock_attach_fd()时增加inode的引用计数上遇到了点小麻烦。    可是当修改到读数据,或者说接收数据相关函数时,问题出现了。Socket读写有两条路可走,就读数据而言,一是socket自身的receive(),二是文件系统的read(),对用户层提供了不同的系统调用接口,其调用过程分别为:
socket sys_recv()->sys_recvfrom()->sock_recvmsg()->__sock_recvmsg()
fs  sys_read()->sock_aio_read()->do_sock_read()->__sock_recvmsg()
    可见,可以在__sock_recvmsg()上将其截获。在socket层,数据包为msghdr结构,所以,只需在函数__sock_recvmsg()中将msg一分为二,则两个进程能同时获得数据。在具体实现过程中,strcut msghdr msg中的msg->msg_iov是数据块指针,msg->msg_iovlen是数据块长度。主进程首先从下层接收数据,封装入msg中,再将msg结构及msg中指针所指的全部数据用memcpy()复制进内核堆区。从进程直接从内核堆区的msg生成自己的msg,绕过下层协议,达到数据一分为二的目的。思路很清晰,可是在测试程序中,主进程能完整地接收到客户端数据,从进程打印出的字符串一直为空。我百思不得其解,对内核函数及复制过程检查了多次,毫无头绪。
    冯师兄不愧为linux内核高手,一语道破玄机,跟一下测试程序中的字符串指针值,看看是不是在内核真的memcpy在这个地址上的字符数组里了。我开始半信半疑,两个进程在被fork后装载的相同的代码,执行结果不一致,怎么会是用户空间指针问题呢?反正想不出什么解决办法,试一试吧,这一跟踪,吓了我一大跳,这个指针值,进了内核果然会改变,在执行完receive之后,地址发生了偏移,向后移动了正好msg->msg_iovlen的长度。这下我全明白了,复制的数据块内容是从空数据开始的,打印出来的当然为空了。仔细一查,果然,在__sock_recvmsg() -> sock_common_recvmsg() -> tcp_recvmsg() -> skb_copy_datagram_iovec() -> memcpy_toiovec()中,我找到了iov->iov_len -= copy; iov->iov_base += copy; 隐藏的够深……
    聪明反被聪明误啊,本想在表面上,以简单的方式达到目的,结果却忽略了实质,走了更大的弯路。另外一个方面,在调试过程中想当然的认为某某地方100%不会有问题,结果问题却恰恰出在那里。
    总结:写程序不能偷懒,不能投机取巧,不能想当然。
    还有,感谢冯师兄。