Socket 是网络协议栈暴露给编程人员的 API,相比复杂的计算机网络协议,API 对关键操作和配置数据进行了抽象,简化了程序编程。
本文讲述的 socket 内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解 socket 编程。
遵循 POSIX.1-2008
标准 c 库,libc, -lc
int shutdown(int sockfd, int how);
shutdown() 调用会将 sockfd 指定的套接字上全双工连接上的一端或者两端关闭。如果 how 指定为 SHUT_RD,那么套接字上将不允许接收;如果 how 指定为 SHUT_WR,那么套接字上将不允许发送。如果 how 是 SHUT_RDWR,那么发送和接收都不允许。
成功时返回 0,失败返回 -1 并设置 errno。
SHUT_RD、SHUT_WR、SHUT_RDWR 的值分别为 0、1、2,在 glibc-2.1.91
how 的合法性检查会在相关的域代码中进行,但是 Linux 3.7 前并不是所有的域都会进行可用性检查。Linux 域套接字直接会忽略不可用的值,Linux 3.7 后这个问题修复了。
遵顼 POSIX.1-2008
标准 c 库,libc, -lc
int close(int fd);
close() 会关闭一个文件描述符,关闭的描述符不再指向任何文件,关闭后可以重新使用该文件描述符。文件上和文件描述符相关的由该进程所有的记录锁(参考 fcntl(2))将会被移除(不管是用哪个文件描述符获得的该锁)。
如果 fd 是最后一个指向底层打开的文件描述的描述符(参考 open(2)),打开文件描述的相关资源都会被释放;如果文件描述符是最后一个指向通过 unlink(2) 移除的文件,那么文件会被删除。
成功时返回 0,失败返回 -1 并设置 errno。
错误列表如下:
EBADF fd 不是一个可用的文件描述符
EINTR close() 被中断打断;参考 signal(7)
EIO I/O 错误发生
ENOSPE、EDQUOT
在 NFS 上,第一次超出可用空间的写不会报告这些错误,而是在接下来的 write(2)、fsync(2) 或者 close(2) 时报告这些错误。可以参考注意部分查看为什么 close() 在报错后不能再次重试。
成功关闭文件描述符并不保证数据成功同步到磁盘上,因为内核会使用缓冲区来做延迟写。通常情况下,文件系统不会在文件关闭时同步这些缓冲区到磁盘上。如果我们想确信数据被存储到底层磁盘,使用 fsync(2) 先进行同步。(从这一点上,也依赖磁盘硬件)。
如果文件描述符标记指定了 close-on-exec,那么就可以保证文件在执行 execve(2) 时自动关闭。参考 fcntl(2) 查看详细信息。
多线程进程和 close()
一个进程中如果有其他线程在使用系统调用访问文件描述符,那么通过 close 关闭文件描述符是非常不明智的。因为文件描述符可以重用,一些条件竞争就可能会导致一些意想不到的结果。
此外,我们可以考虑下面两个线程操作同一个文件描述符的场景:
(1)一个线程正在阻塞在文件描述符的系统调用上,比如向一个满了的管道尝试 write(2) 写,或者从一个没有数据的流套接字上 read(2) 读。
(2)另一个线程关闭这个文件描述符
这种情况在不同的系统上的行为是不一样的,一些系统上一旦文件描述符关闭,其他阻塞系统调用就会返回错误。
在 Linux 以及另一个系统上,行为是不同的:阻塞的 I/O 系统调用一直持有底层打开的文件描述,直到 I/O 系统调用结束。(参考 open(2) 关于打开文件描述的讨论。)因此,阻塞系统调用完全可能在 close() 后仍然成功返回。
close() 返回错误处理
编程人员需要检查 close() 的返回值,因为很可能之前 write(2) 的错误只能在最后的 close() 要释放文件描述时才会报告。不检查错误返回值可能会导致一些数据偷偷的丢掉,这种情况在 NFS 和磁盘配额时是常见的。
然而这些错误返回值只能用来诊断(即向应用发出警告:可能仍然有一些等待的 I/O 或者 I/O 失败)以及重播目的(即再写一次文件或者创建备份)。
失败后重试 close() 是不可行的,因为文件描述符可能会被其他线程重用,导致误关闭其他线程文件描述符,这主要是因为内核会首先释放文件描述符,然后再进行一些会返回错误的操作,比如将数据刷新到文件系统或设备。
一些其他系统实现也会再报告错误的情况下关闭文件描述符(除了 EBADF,表示文件描述符不可用)。POSIX.1 目前对这点并没有提出什么,不过可能在下一个主版本会授权这种做法。
我们可以通过在发生错误的 close(2) 后调用 fsysnc(2) 来了解具体是发生了什么 I/O 错误。
EINTR 错误有点特殊,关于这个错误,POSIX.1-2008 的说法是:
如果 close() 被能被捕捉到的信号中断,它应该返回 -1 并将 errno 设置为 EINTR,fildes 状态未指定
这就允许 Linux 以及其他实现上可以像处理其他错误一样处理这个错误,文件描述符保证被关闭了。然而,也允许返回 EINTR 后仍然保持文件描述符是打开的。(HP-UX 的 close() 就是这样的。)调用者必须重新使用 close() 来关闭文件描述符,避免文件描述符泄露。这种实现的不同导致了程序移植性问题,因为其他实现在失败后不允许重新 close()。下一个 POSIX.1 的主版本也会解决这个问题。