从unix类套接口看stream与dgram区别及抽象套接口
一、unix套接字
这 种形式的套接字和通常的计算机间通讯不同,它是用来进行计算机内部进程间通讯的一种方式。大家比较经常接触到的进程间通讯方式可能是管道(无名和命名)、 消息队列、共享内存等,可能对这个使用的比较少。那么我们可以想象一下这种通讯方式和前几种通讯方式相比,它的特殊之处在哪里?
作为一个套接字, 不管它是用文件的形式呈现,还是以socket的形式呈现,它总归具有socket的基因,不然它就不放在内核的net文件夹下了。一个socket的特 征就是一个client/server的模式,而server的一个重要特点就是在某一个特定的地址只能有一个侦听服务者,而客户端是未知的任意多。想一 下消息队列和命名管道,它的接收者和发送者都是不确定的多对多关系。
那么可能有些同学会觉得,只要大家约定好,侦听的只有一个就好了,没必要专门折腾一个新的套接口吧。事实上很多事情的确是必须要使用制度保证的,就像我们原来假设如果大家遵守道德就可以避免社会丑恶一样,骚年,你太天真了。
另 一方面,一个最为合适模型能够准确的反映出一个真实的世界,同样可以减少对于工程理解的问题。如果可能存在多对多的模型,那么有些人可能就会猜测这个地方 是不是故意为了实现之后的多对多,合适的才是最好的。考虑一下常用的桌面系统,里面很多的功能都应该是一个人来提供集中式服务的,仅此一家,绝无分店。例 如对于整个桌面的裁剪,例如系统的剪切板。随便打开一个gnome桌面系统,大家应该可以看到很多的unix类型的套接字。
二、绑定同一个端口时何处出错返回
事 实上之前的很多描述我几乎查看资料,只是信口开河,但是我不是一个随便的人,所以我还是要验证一下,google一下unix socket找一个例子,验证了一个套接口只能有一个bind操作的实施,这没有什么,写个程序测试一下就可以完成,你甚至不用写程序,在网上下载一个源 代码编译运行一下即可。但是这都不是我们想要的,真正想要的不仅是猜得到开始,猜得到结局,好药猜得到过程。那么这里的问题就是,对于bind系统系统调 用,到达内核之后是从哪里错误返回的呢?
同样是看了一下代码,我的猜测是从 unix_bind---->>>vfs_mknod--->>>ext2_mknod--->>>ext2_add_nondir--->>>ext2_add_link
err = -EEXIST;
if (ext2_match (namelen, name, de))
goto out_unlock;
我 猜测是从这里返回的。同样,作为一个严谨的人,我还是要验证一下,因为今天看了似懂非懂的看了一部电影叫做《蝴蝶效应》,内容并不重要,重要的是此时此刻 我想表达的意思是:一个错误可能会引发更多的错误。老实说,测试例子并不是我从头写的,同样是拷贝下来直接编译测试的,由于这种代码到网上随便一搜就有, 所以我就不在这里再列一份,增加互联网的负担了:
(gdb) s
may_create (nd=0x0, child=0xcfe2246c, dir=0xcfbccb8c) at fs/namei.c:1427
1427 if (child->d_inode)
(gdb) n
1428 return -EEXIST;
(gdb) bt
#0 may_create (nd=0x0, child=0xcfe2246c, dir=0xcfbccb8c) at fs/namei.c:1428
#1 vfs_mknod (nd=0x0, child=0xcfe2246c, dir=0xcfbccb8c) at fs/namei.c:1841
#2 0xc079c43e in unix_bind (sock=0xcf2d1980, uaddr=0xcff99ecc, addr_len=13)
at net/unix/af_unix.c:811
#3 0xc06d6119 in sys_bind (fd=3, umyaddr=0xbfcf9f0a, addrlen=12) at net/socket.c:1302
#4 0xc06d7504 in sys_socketcall (call=2, args=0xbfcf9ef0) at net/socket.c:1992
#5 0xc0107a84 in ?? ()
#6 0x00000002 in ?? ()
#7 0xbfcf9ef0 in ?? ()
#8 0x00000000 in ?? ()
(gdb)
可见虽然猜对了结局,但是没有猜对过程,它的结果是在中间一个不起眼的地方返回的,而根本没有达到底层的文件系统,也就是在通用的vfs文件系统层就完成了这个判断和检测。
三、stream和dgram的区别
1、发送不同
事实上我也没有看出有什么比较明显的不同的地方,最为明显的地方是stream发送的时候必须是已经执行过connect操作而dgram则没有这个检测。
另一个就是在超时等待的时间上,两者在发送skb的申请上都进行了判断,那就是一个socket不可能占用系统中所有的skb(也就是内存)资源,所以如果这个接收的另一端一直没有接收,那么这个地方就需要在申请skb的地方阻塞。
但是对于stream来说,如果skb申请完成,那么就不存在超时的问题,这个消息一定是会到达对方的。相反,对于dgram来说,即使申请到了skb,在发送的时候它同样会存在一个超时检测。下面是dgram的代码
if (unix_peer(other) != sk &&
(skb_queue_len(&other->sk_receive_queue) >
other->sk_max_ack_backlog)) {
if (!timeo) {
err = -EAGAIN;
goto out_unlock;
}
timeo = unix_wait_for_peer(other, timeo);
err = sock_intr_errno(timeo);
if (signal_pending(current))
goto out_free;
goto restart;
}
skb_queue_tail(&other->sk_receive_queue, skb);
不过大家也不用大惊小怪,因为stream其实也有这个检测,只是它的检测并不是在发送的时候完成检测,而是在connect的地方完成的检测:
static int unix_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
……
if (skb_queue_len(&other->sk_receive_queue) >
other->sk_max_ack_backlog) {
err = -EAGAIN;
if (!timeo)
goto out_unlock;
timeo = unix_wait_for_peer(other, timeo);
大 家可能比较好奇,这里的timeo的默认值是多少呢?默认是一个比较大的值,也就是最大的正整数,如果以秒为单位,在这么长的时间内,太阳可能已经爆炸了 (好像记得太阳的寿命是50亿年?)unix_create1--->>>sock_init_data
#define LONG_MAX ((long)(~0UL>>1))
#define MAX_SCHEDULE_TIMEOUT LONG_MAX
sk->sk_rcvtimeo = MAX_SCHEDULE_TIMEOUT;
sk->sk_sndtimeo = MAX_SCHEDULE_TIMEOUT;
2、接收不同
这个是一个比较有意思的不同,所以大家要注意。
static int unix_dgram_recvmsg(struct kiocb *iocb, struct socket *sock,
struct msghdr *msg, size_t size,
int flags)
if (size > skb->len)
size = skb->len;
else if (size < skb->len)
msg->msg_flags |= MSG_TRUNC;
err = skb_copy_datagram_iovec(skb, 0, msg->msg_iov, size);
对应地,字节流的处理过程为
static int unix_stream_recvmsg(struct kiocb *iocb, struct socket *sock,
struct msghdr *msg, size_t size,
int flags)
skb = skb_dequeue(&sk->sk_receive_queue);
……
chunk = min_t(unsigned int, skb->len, size);
copied += chunk;
size -= chunk;
/* Mark read part of skb as used */
if (!(flags & MSG_PEEK))
{
……
/* put the skb back if we didn't use it up.. */
if (skb->len)
{
skb_queue_head(&sk->sk_receive_queue, skb);
break;
}
kfree_skb(skb);
}
这个明显的区别就是:对于dgram来说,如果此次调用给出的接收空间小于报文大小,那么多出来的部分会从系统中丢弃,所以说是比较败家的;而字节流的操作则比较节俭,需要取多少就取多少,剩余的部分再次放回缓冲区,等待下一次读取。
但是这里大家要和IP层的报文分段(frag)区分开来,dgram格式的报文同样会在ip层分段,如果说报文比较大的话。
四、抽象套接字
下面这些套接字的特点就是都是以@开始的,显然它们是有故事的。
[tsecer@Harry strorint]$ cat /proc/net/unix | grep @
ed5c3600: 00000002 00000000 00010000 0001 01 12469 @/tmp/.ICE-unix/1498
ee898400: 00000002 00000000 00010000 0001 01 9473 @/var/run/hald/dbus-c73WZHqTNc
f650f400: 00000002 00000000 00000000 0002 01 5039 @/com/ubuntu/upstart
f64a8400: 00000002 00000000 00010000 0001 01 11135 @/tmp/.X11-unix/X0
ee85fe00: 00000002 00000000 00010000 0001 01 9450 @/var/run/hald/dbus-Q80GTMdfVP
f6a79000: 00000002 00000000 00000000 0002 01 5855 @/org/kernel/udev/udevd
ed513000: 00000002 00000000 00010000 0001 01 11360 @/tmp/gdm-session-kwySxIoQ
ee8afc00: 00000002 00000000 00000000 0002 01 9518 @/org/freedesktop/hal/udev_event
ed500c00: 00000002 00000000 00010000 0001 01 11227 @/tmp/gdm-greeter-dgvIxciO
ed51a600: 00000002 00000000 00010000 0001 01 12359 @/tmp/dbus-nwC7rTEfI5
dc5afe00: 00000003 00000000 00000000 0001 03 455530 @/tmp/.X11-unix/X0
1、如何创建
创 建的方法也比较简单,就是在设置套接字地址的时候开始的第一个字符是字符串结束的'\0'标志。所有的C程序员都知道这是一个字符串结束的标志,所以放在 一个路径的开始是非常的不厚道的。但是这里通过另外辅助的结构来表示它之后还有一个字符串,那就是通过name_len来表示,如果说name_len大 于零而路径的第一个字符为空(或者path是一个空字符串),那么这个0之后的路径才是真正的路径。
2、为什么有这种套接字
问 题在于很多时候不同的用户是可以被chroot的,这个最为常见的就是FTP的客户端,一个用户可能会设置不同的根目录,这样在这个用户看文件系统就有一 个坐井观天的感觉,它看到的文件系统和整个系统中的文件系统并不相同。例如"/tmp/xxxx",因为它看到的是系统中一个子目录,所以这些用户通过这 样的路径就无法找到约定好的侦听套接口。
为了解决这个问题,就引入了抽象套接口(abstract socket)的概念,也就是内核并不会真正的到路径中指定的地方(不同的文件系统)来创建文件,而是把路径作为唯一的一个字符串标志来进行hash,这 样绕过文件系统而使用最为原始的字符串作为套接口的唯一标示,这样不同的用户及时被切换了根目录,只要它们使用的字符串相同,那么就可以找到这些套接口。
3、相关处理代码展示
if (!sunaddr->sun_path[0]) {抽象套接口处理,只是根据名字(字符串比较)。
err = -EADDRINUSE;
if (__unix_find_socket_byname(sunaddr, addr_len,
sk->sk_type, hash)) {
unix_release_addr(addr);
goto out_unlock;
}
list = &unix_socket_table[addr->hash];
} else {//非抽象,直接使用文件系统的inode,之前的文件系统路径查找一个完成。
list = &unix_socket_table[dentry->d_inode->i_ino & (UNIX_HASH_SIZE-1)];
u->dentry = nd.dentry;
u->mnt = nd.mnt;
}