当前位置: 首页 > 科技观察

从内核看文件描述符传递的实现

时间:2023-03-12 16:29:35 科技观察

文件描述符是内核提供的一个非常有用的技术,一般在服务器中,主进程负责接收请求,然后将请求传递给子进程进行加工。本文分析文件描述符传输在内核中是如何实现的。我们先来看看文件描述符传输是什么概念。假设主进程打开了一个文件,我们来看一下进程与文件系统的关系。如果此时主进程fork出一个子进程,结构如下。我们看到主进程和子进程都指向同一个文件。所以如果此时live进程打开另外一个文件。架构如下。当我们看到新打开的文件时,子进程将不再指向它。假设文件的底层资源是一个TCP连接,主进程想把这个关系同步给子进程,也就是交给子进程,那怎么办呢?这时候,我们就需要用到文件描述符传输了。下面是我们期望的架构。通常主进程会关闭对应的文件描述符,也就是解除引用关系。但是这里我们可以忽略这一点。文件描述符的能力不是天生的,需要内核支持。如果我们简单地将fd(文件描述符)作为数据传递给子进程,则子进程无法指向对应的文件。让我们看看我们如何使用这种技术,看看如何通过内核来实现它。下面使用的代码取自Libuv。intfd_to_send;//核心数据结构structmsghdrmsg;structcmsghdr*cmsg;union{chardata[64];structcmsghdralias;}scratch;//获取要发送的文件描述符fd_to_send=uv__handle_fd((uv_handle_t*)req->send_handle);memset(&scratch,0,sizeof(scratch));msg.msg_name=NULL;msg.msg_namelen=0;msg.msg_iov=iov;msg.msg_iovlen=iovcnt;msg.msg_flags=0;msg.msg_control=&scratch.alias;msg.msg_controllen=CMSG_SPACE(sizeof(fd_to_send));cmsg=CMSG_FIRSTHDR(&msg);cmsg->cmsg_level=SOL_SOCKET;cmsg->cmsg_type=SCM_RIGHTS;cmsg->cmsg_len=CMSG_LEN(size{of(fd_to_send));/*silence/aliasingwarvoid*pv=CMSG_DATA(cmsg);int*pi=pv;*pi=fd_to_send;}//fd为Unix域对应的文件描述符intfd=uv__stream_fd(stream);//发送文件描述符sendmsg(fd,&味精,0);我们看到发送文件描述符比较复杂,主要使用的数据结构是msghdr。将要发送的文件描述符保存在msghdr中并设置一些标志。然后通过Unix域发送(Unix是唯一支持文件描述符传递的进程间通信方式)。下面我们下来主要分析内核对sendmsg的实现。caseSYS_SENDMSG:err=__sys_sendmsg(a0,(structuser_msghdr__user*)a1,a[2],true);系统调用对应__sys_sendmsg。long__sys_sendmsg(intfd,structuser_msghdr__user*msg,unsignedIntflags,booolforbid_cmsg_compat){intfput_needed,err;structmsghdrmsg_sys;structmsghdrmsg_sys;structssocket*sock_sock;///////////structs;//////////,0);}后面的链接很长syssendmsg->__sys_sendmsg->sock_sendmsg->sock_sendmsg_nosec。staticinlineintsock_sendmsg_nosec(structsocket*sock,structmsghdr*msg){intret=INDIRECT_CALL_INET(sock->ops->sendmsg,inet6_sendmsg,inet_sendmsg,sock,msg,msg_data_left(msg));BUG_ON(ret==-EIOCBQUEUED);}returnre看到了最后调用sock->ops->sendmsg,我们看一下sendmsg在Unix域的实现。Unix域中有SOCK_STREAM和SOCK_DGRAM两种模式,我们可以选择第一种进行分析。staticintunix_stream_sendmsg(structsocket*sock,structmsghdr*msg,size_tlen){structsock*sk=sock->sk;structsock*other=NULL;interr,size;structsk_buff*skb;intsent=0;structscm_cookiescm;boolfds_sent=false;intdata_len;//复制文件描述符信息到scmscm_send(sock,msg,&scm,false);//通信的另一端other=unix_peer(sk);//不断构建数据包skbuff发送,直到发送完成while(sentmsg_flags&MSG_DONTWAIT,&err,get_order(UNIX_SKB_FRAGS_SZ));//复制scm到skberr=unix_scm_to_skb(&scm,skb,!fds_sent);//将数据写入skberr=skb_copy_datagram_from_iter(skb,0,&msg->msg_iter,size);//插入peer的消息队列skb_queue_tail(&other->sk_receive_queue,skb);//通知peer有数据读取other->sk_data_ready(other);sent+=size;}//...}我们看到整体逻辑不负责,主要是根据数据构造skb结构,然后插入peer消息队列,最后通知对端有消息可用Read,我们这里只关注文件描述符的处理。首先我们看一下scm_send。static__inline__intscm_send(structsocket*sock,structmsghdr*msg,structscm_cookie*scm,boolforcecreds){memset(scm,0,sizeof(*scm));scm->creds.uid=INVALID_UID;scm->creds.gid=INVALID_GID;unix_get_peersec_dgram(sock,scm);if(msg->msg_controllen<=0)return0;return__scm_send(sock,msg,scm);}int__scm_send(structsocket*sock,structmsghdr*msg,structscm_cookie*p){structcmsghdr*cmsg;interr;for_each_cmsghdr(cmsg,msg){switch(cmsg->cmsg_type){caseSCM_RIGHTS:err=scm_fp_copy(cmsg,&p->fp);if(err<0)gotoerror;break;}}}我们看到__scm_send遍历了要发送的数据,然后判断cmsg->cmsg_type的值,这里我们是SCM_RIGHTS(见初始使用代码),然后调用scm_fp_copy。staticintscm_fp_copy(structscmsghdr*cmsg,structscm_fp_list**fplp){int*fdp=(int*)CMSG_DATA(cmsg);structscm_fp_list*fpl=*fplp;structfile**fpp;inti,num;num=(cmsg->cmsg_len-sizeof(structcmsghdr))/sizeof(int);if(!fpl){fpl=kmalloc(sizeof(structscm_fp_list),GFP_KERNEL);*fplp=fpl;fpl->count=0;fpl->max=SCM_MAX_FD;fpl->user=NULL;}fpp=&fpl->fp[fpl->count];//遍历并将fd对应的文件保存到fpp中。for(i=0;icount++;}if(!fpl->user)fpl->user=get_uid(current_user());returnnum;}我们看到scm_fp_copy将fd对应的文件遍历保存到fpp中。而fpp属于fpl属于fplp属于第一个structscm_cookiescm(unix_stream_sendmsg函数),也就是最后将fd对应的文件存入scm。然后我们回到unix_stream_sendmsg中去查看unix_scm_to_skb。staticintunix_scm_to_skb(structscm_cookie*scm,structsk_buff*skb,boolsend_fds){interr=0;UNIXCB(skb).pid=get_pid(scm->pid);UNIXCB(skb).uid=scm->creds.uid;UNIXCB(skb).gid=scm->creds.gid;UNIXCB(skb).fp=NULL;unix_get_secdata(scm,skb);if(scm->fp&&send_fds)//写入skberr=unix_attach_fds(scm,skb);skb->destructor=unix_destruct_scm;returnerr;}再看unix_attach_fds。intunix_attach_fds(structscm_cookie*scm,structsk_buff*skb){inti;//复制到skbUNIXCB(skb).fp=scm_fp_dup(scm->fp);return0;}structscm_fp_list*scm_fp_dup(structscm_fp_list*fpl){structscm_fp_list*new_fpl;new_fpl=kmemdup(fpl,offsetof(structscm_fp_list,fp[fpl->count]),GFP_KERNEL);if(new_fpl){for(i=0;icount;i++)get_file(fpl->fp[i]);new_fpl->max=new_fpl->count;new_fpl->user=get_uid(fpl->user);}returnnew_fpl;}至此我们看到数据和文件描述符都写入skb,并插入到peer消息队列。然后分析对端是如何处理的。我们从recvmsg函数开始,对应Uinix域实现unix_stream_recvmsg。staticintunix_stream_recvmsg(structsocket*sock,structmsghdr*msg,size_tsize,intflags){structunix_stream_read_statestate={.recv_actor=unix_stream_read_actor,.socket=sock,.msg=msg,.size=size,.flags=flags};returnunixst_genericam(&true);}然后看staticintunix_stream_read_generic(structunix_stream_read_state*state,boolfreezable){structscm_cookiescm;structsocket*sock=state->socket;structsock*sk=sock->sk;structunix_sock*u=unix_sk(sk);//getaskbskb=skb_peek(&sk->sk_receive_queue);//队列skb_unlink(skb,&sk->sk_receive_queue);//拷贝skb数据到state->msgstate->recv_actor(skb,skip,chunk,state);//Processfiledescriptorif(UNIXCB(skb).fp){scm_stat_del(sk,skb);//将skb的文件描述符信息复制到scmunix_detach_fds(&scm,skb);}if(state->msg)scm_recv(sock,state->msg,&scm,flags);}内核首先通过钩子函数recv_actor将skb数据拷贝到state->msg中,recv_actor对应的函数是unix_stream_read_actor。然后看ach_fix_det。voidunix_detach_fds(structscm_cookie*scm,structsk_buff*skb){inti;//写入scmscm->fp=UNIXCB(skb).fp;UNIXCB(skb).fp=NULL;}unix_detach_fds写入文件描述符信息到scm.最后调用scm_recv处理文件描述符。static__inline__voidscm_recv(structsocket*sock,structmsghdr*msg,structscm_cookie*scm,intflags){scm_detach_fds(msg,scm);}voidscm_detach_fds(structmsghdr*msg,structscm_cookie*scm){intfdmax=min_t(int,scm_max_sf>msgs),-(scmfp->count);inti;for(i=0;ifp->fp[i],cmsg_data+i,o_flags);if(err<0)break;}}scm_detach_fds调用receive_fd_user来处理文件描述符。第一个入参scm->fp->fp[i]是文件结构体,需要传递给文件描述符对应的文件。让我们看看进展如何。staticinlineintreceive_fd_user(structfile*file,int__user*ufd,unsignedinto_flags){return__receive_fd(-1,file,ufd,o_flags);}int__receive_fd(intfd,structfile*file,int__user*ufd,unsignedinto_flags){intnew_fd;//fd为-1,申请一个新的if(fd<0){new_fd=get_unused_fd_flags(o_flags);}else{new_fd=fd;}if(fd<0){fd_install(new_fd,get_file(file));}else{//...}returnnew_fd;}我们看到首先应用了当前进程的新文件描述符。然后调用fd_install关联文件。voidfd_install(unsignedintfd,structfile*file){//current->files是当前进程打开的文件描述符列表__fd_install(current->files,fd,file);}void__fd_install(structfiles_struct*files,unsignedintfd,structfile*file){structfdtable*fdt;//获取管理文件描述符的数据结构fdt=rcu_dereference_sched(files->fdt);//给指向file的元素赋值rcu_assign_pointer(fdt->fd[fd],file);}最终形成下图所示的架构。后记,我们看到文件描述符传输的核心是在发送的数据中记录要传输的文件描述符对应的文件结构,然后发送并标记,然后在接收的过程中,重新创建一个新的fd传递文件的关联关系。