linux socket学习

1. unix sock中,unix_find_socket_byname中使用sock_hold是为了在连接和发送消息时,增加对对端sk的引用,

避免对端sk忽然退出了

2. 加入hlist,intert里仅是 sock_hold 增加对sock的引用。另外就是基本功能:将节点增加到hlist的头

3. remove时不用list参数,是因为remove的动作很简单:将这个节点的前一节点指向这个节点的后一节点就行了!

也就是将sk脱链

sk_hashed 是判断sk是否有前节点,也就是这个sk是不是处于hlist中

4. security_sk_alloc 是在sk alloc时执行的

5. sk_alloc没有初始化 sk_refcnt, sk_refcnt在sock_init_data中设置,sock_init_data是用socket的一些数据进行初始化

6. mercury 只需 kmalloc即可,然后初始化 sk_refcnt,其它值不用管了,因为没有用到这个sk

7. asmlinkage 是GCC对C程序的一种扩展, #define asmlinkage __attribute__((regparm(0)))

表示用0个寄存器传递函数参数,这样,所有的函数参数强迫从栈中提取。

这个asmlinkage大都用在系统调用中,系统调用需要在entry.s文件中用汇编语言调用,所以必须要保证它符合C语言的参数传递规则,才能用汇编语言正确调用它。

syscall在38版本与之前不一样了,由SYSCALL_DEFINEx来定义,x为参数个数。如:

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)

表示定义的是sys_socketcall,参数个数为2,int是第一个参数的类型,call是第一个参数的名称。以此类推。

8. Unix域的插口地址有两种类型。一种是常规的路径名字符串,不过不一定是以0结尾,在长度中也不包括结尾的0在内;另一种是以“\0”开头的,称为抽象地址。对于前者,unix_mkname()将其转换成一个以0结尾的字符串,并对其长度作出相应调整(195~196行)。后者类似于网络地址,unix_mkname()为之计算出一个杂凑值,并通过参数hashp返回这个杂凑值。 

对常规的字符串地址,unix_bind()根据其路径名为之在文件系统中建立一个“文件”节点。像其他特殊文件一样,这个文件实际上只是一个索引节点,而并没有用于数据的记录块,用索引节点号代替地址的杂凑值来决定将插口的sock数据结构挂入杂凑表中的哪个队列。

为什么要建立这个文件呢?这是因为Unix域允许以常规的路径名作为插口地址,这样的插口地址便于记忆,也便于通过常规的文件操作来检查一个特定的路径名是否已经在使用中。如果文件系统中已经存在具有相同路径名的文件,则unix_bind()会失败而返回出错代码EADDRINUSE。所以通常在用户程序中要在调用bind()之前先调用unlink(),将可能已经存在的同名文件先删除。应该指出,插口并不是持续存在的,其寿命决不会超过创建它的进程。当创建插口的进程exit()时,它所创建的插口也会随着已打开文件的关闭而消失。可是,为插口创建的文件(节点)却是持续存在的;即使创建它的进程exit(),甚至机器已经断电,这文件还是存在于磁盘上,所以必须特地加以删除。

9. sendmsg, recvmsg, write, read

第一个界面是为插口专设的,从用户程序的角度来看就是三对libc库函数,即recv()/send()、recvfrom()/sendto()以及recvmsg()/sendmsg()

第二个界面是通过常规的文件操作read()和write()这两个系统调用来进行

两个界面上的这些函数最后都是殊途同归,都归结到sock_recvmsg()和sock_sendmsg()两个函数。

从语义的角度来说,一般对“有连接”插口倾向于使用read()/write(),而对“无连接”插口则通常都使用recvfrom()/sendto()等函数。这是因为在“有连接”模式的通信中将传递的数据看成连续的“字节流”,而不保留“报文”的边界(所以其类型称为SOCK_STREAM),与文件操作的语义比较贴近。反之,“无连接”模式的通信则是“面向报文”的,所以保留报文的边界。 

* socket 的撤消

 关闭、撤销一个插口时,其sock结构中的计数refcnt有可能还大于1,表示还有用户,所以不能马上将这个结构释放,而只能将其refcnt计数减1,把释放结构的责任留给最后将这个计数减到0的那个进程。但是,光凭计数refcnt不足以说明该插口在逻辑上是否已经撤销,所以在sock结构中又设置了一个标志量dead,表示尽管该sock结构中的refcnt还不是0,所以还不能把数据结构最后释放,但实际上插口已经不存在了。 

光是对sock结构的使用和释放加以保护还不够,还要防止对sock结构的使用(例如报文的到达)和撤销在时间上相重叠。也就是说,这二者在时间上必须加以“串行化”。这样,如果插口的撤销在前,那就让撤销的过程先完成,然后再来撤销,因为在撤销的过程中可能需要对使用的后果(例如链入到receive_queue队列中的报文)加以善后处理。为此目的,内核中设置了两对加锁/解锁操作,即unix_state_rlock()/unix_state_runlock()和unix_state_wlock()/unix_state_wunlock()。当一个进程要读取sock结构中的状态信息(特别是dead)时,要先调用unix_state_rlock()加锁。这样,如果另一个进程正想要改变sock结构中的状态信息(例如想把dead变成1),就要在一个循环中(并不睡眠!)等待解锁后才能继续

* sock_wake_async(),

是干什么用的呢?让我们回顾一下server方进程是怎样通过accept()来接受连接请求的。大家知道,accept()是一个server插口接受连接请求的唯一途径,server插口是不能主动要求连接的。同时,accept()的操作从本质上说是“同步”的,如果调用accept()时尚无连接请求到来,就要睡眠等待。诚然,server方的进程可以通过O_NOBLOCK标志让accept()在没有连接请求时立即返回,但这样一来,server方进程就只好循环地或者定期地调用accept()来测试是否有连接请求到来。再考虑有时候server方进程要同时照看好几个server插口的情况,这时候server方进程就只好将O_NONBLOCK标志设成1来“轮循”各个server插口了。但是,不管时睡眠也好,或是轮循也好,server进程在此期间就不能做别的事情了。所以,O_NONB

在文件操作ioctl()中就设置了一条命令FIOASYNC,让有关进程(必须是文件的主人)可以通过这条命令向一个文件挂上号,让它在某种条件得到满足时就向该进程发送一个信号。为了这个目的,在插口的socket结构中设置了一个队列fasync_list。当server方进程希望异步地等待连接时,就通过ioctl()的FIOASYNC命令(插口也是一个已打开文件)将一个fasync_struct结构挂入到这个队列中。另一方面,我们以前也讲过,代表着一个插口的file结构中有个指针f_op,指向一个file_operations结构socket_file_ops。这个结构中的指针fasync指向函数sock_fasync(),当server方进程通过ioctl()发出FIOASYNC命令时,就会执行这个函数来完成上述将一个fasync_struct数据结构挂入到这个队列中的操作。

* accept

虽然这里所说插口都是“有连接”模式的,但那只是对数据(报文)而言,而控制报文实际上都是“无连接”的(否则,靠什么手段来建立最初的连接呢?)所以,这里通过skb_recv_datagram()从receive_queue队列中接收代表着连接请求的控制报文。注意datagram(“数据报”)并不表示“数据报文”,而是说以“无连接”模式传递的报文

* sock结构是内核中常常要动态分配是使用的,所以内核中为此专设了一个队列,通过slab机制来管理这种数据结构的缓冲区。

* sk_buffer

在sock结构中有几个双向队列,其中最重要的就是receive_queue和write_queue,而error_queue则仅在网络环境下才会用到。这几个队列并不采用通用的队列头结构list_head,而专门定义了一种sk_buff_head数据结构.

通常所说的“报文”是ISO的7层模型中第4层,即“传输层”的概念;

“网络层”的数据单位,称为packet(“报文分组”,或“包”)。

在Unix域的条件下,由于不涉及网络介质,所以一个报文就是一个包。每一个包都要占用一个sk_buff数据结构,所以receive_queue队列中的每个sk_buff数据结构就载运着一个到达的包,而write_queue队列中则为待发送的包。

在sock结构中还有一个特殊的sk_buff结构队列(sk_async_wait_queue),那是专为网络环境而设置的,我们在这里并不关心。

*

rcvbuf和sndbuf分别为接收和发送缓冲区的大小 32K

* Unix域中的插口地址即为一个文件(节点)的路径名。注意在unix_address结构中的name[]数组大小为0,所以unix_address结构的大小并不包括sockaddr_un结构的空间,在分配空间时要额外加上。

* 无连接也可以connect

事实正是这样,对于“无连接”模式的插口,可以用connect()先设置一个对方地址,然后再用send()发送报文,而实际上每次都使用预先设置好的对方地址。但是要注意,在“无连接”模式中使用connect()与在“有连接”模式中使用connect()有本质的区别。在“无连接”模式中,connect()的作用只是让内核为“本地”插口记下预设的对方地址,而并不涉及与对方之间控制报文的交互。以后则在发送的每个报文头部附上这个地址,以指明报文的目的地。至于在“有连接”模式中的connect(),则实际向对方发送一个请求连接的控制报文(指在网络环境下),并等待对方的响应。连接建立了以后,随同每个报文发送的可以只是一个连接号,而不一定要包括对方地址。

* msg_iov

msg_iov则指向一个结构数组,该数组中的每一个元素都是一块数据,这样一个报文的内容就可以分散在若干个互不连续的缓冲区中,而在逻辑上却连在一起,在网络环境下这是很有好处的。还有,msg_control和msg_controllen的作用是传递控制信息,在Unix域中用来在进程间传递访问权限,还可以用来传递“打开文件描述体”。

* 什么是RCU,在socket里用到了RCU

RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它 时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时 机就是所有引用该数据的CPU都退出对共享数据的操作。

因此RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,而且在除alpha的所有架构上也不需要内存 栅(Memory Barrier),因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它需要 延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。读者必须提供一个信号给写者以便写者能够确定数据可以 被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集 器就调用回调函数完成最后的数据释放或修改操作。 RCU与rwlock的不同之处是:它既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并 行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代rwlock,因为如果 写比较多时,对读者的性能提高不能弥补写者导致的损失。

相关推荐