上一篇文章我们分析了shutdown方法的实现,这里我们再看下close方法。
// fs/open.c
SYSCALL_DEFINE1(close, unsigned int, fd)
{
int retval = __close_fd(current->files, fd);
...
return retval;
}
EXPORT_SYMBOL(sys_close);
该方法调用了__close_fd方法。
// fs/file.c
int __close_fd(struct files_struct *files, unsigned fd)
{
struct file *file;
struct fdtable *fdt;
...
fdt = files_fdtable(files);
...
file = fdt->fd[fd];
...
return filp_close(file, files);
...
}
该方法先通过fd找到对应的file,再调用filp_close方法对file进行close。
// fs/open.c
int filp_close(struct file *filp, fl_owner_t id)
{
int retval = 0;
...
fput(filp);
return retval;
}
EXPORT_SYMBOL(filp_close);
该方法又调用了fput方法。
// fs/file_table.c
void fput(struct file *file)
{
if (atomic_long_dec_and_test(&file->f_count)) {
struct task_struct *task = current;
if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) {
init_task_work(&file->f_u.fu_rcuhead, ____fput);
if (!task_work_add(task, &file->f_u.fu_rcuhead, true))
return;
...
}
...
}
}
方法描述
1. 先将file->f_count字段减1,再判断该字段的值是否为0,如果是,则继续执行if内的逻辑。
2. 调用init_task_work方法,设置file销毁的回调函数为____fput。
3. 调用task_work_add方法,将销毁该文件的task放到待执行的任务队列中。
最终____fput方法会被回调,继续执行文件的close逻辑。
// fs/file_table.c
static void ____fput(struct callback_head *work)
{
__fput(container_of(work, struct file, f_u.fu_rcuhead));
}
该方法会先根据work指针以及该指针所属字段在struct file中的偏移量,找到对应的file指针,然后再调用__fput方法,传入这个file参数。
// fs/file_table.c
static void __fput(struct file *file)
{
...
eventpoll_release(file);
...
if (file->f_op->release)
file->f_op->release(inode, file);
...
}
该方法先调用eventpoll_release方法,检查该文件是否已被注册到epoll实例中,如果是则从epoll实例中移除。
之后再调用file->f_op->release指向的方法,由第一篇文章可以知道,该方法是sock_close。
// net/socket.c
static int sock_close(struct inode *inode, struct file *filp)
{
__sock_release(SOCKET_I(inode), inode);
return 0;
}
该方法又调用了__sock_release方法。
// net/socket.c
static void __sock_release(struct socket *sock, struct inode *inode)
{
if (sock->ops) {
...
sock->ops->release(sock);
...
}
...
}
该方法又调用了sock->ops->release指向的方法,由第一篇文章可以知道,该方法是inet_release。
// net/ipv4/af_inet.c
int inet_release(struct socket *sock)
{
struct sock *sk = sock->sk;
if (sk) {
long timeout;
...
timeout = 0;
...
sk->sk_prot->close(sk, timeout);
}
return 0;
}
EXPORT_SYMBOL(inet_release);
该方法又调用了sk->sk_prot->close指向的方法,由第一篇文章可以知道,这个方法是tcp_close。
// net/ipv4/af_inet.c
void tcp_close(struct sock *sk, long timeout)
{
struct sk_buff *skb;
int data_was_unread = 0;
...
sk->sk_shutdown = SHUTDOWN_MASK;
...
while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {
u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq;
...
data_was_unread += len;
__kfree_skb(skb);
}
...
if (unlikely(tcp_sk(sk)->repair)) {
...
} else if (data_was_unread) {
...
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, sk->sk_allocation);
} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
...
} else if (tcp_close_state(sk)) {
...
tcp_send_fin(sk);
}
...
}
EXPORT_SYMBOL(tcp_close);
方法描述
1. 设置变量data_was_unread的值为0,该变量用于表示tcp recvbuf中还有多少字节未读。
2. 设置sk->sk_shutdown字段值为SHUTDOWN_MASK,表示RCV和SEND都已经shutdown。
3. 清空sk->sk_receive_queue队列中的数据,并统计还有多少字节未读。
4. 如果未读字节数大于0,则直接将sk状态设置为TCP_CLOSE,并发送reset消息给对方。
5. 如果未读字节数等于0,则调用tcp_close_state方法,根据当前sk状态设置sk下一状态,比如,假设当前sk状态为TCP_ESTABLISHED,则下一状态为TCP_FIN_WAIT1,该方法的返回值为,是否要发送fin消息给对方。
6. 如果需要,则调用tcp_send_fin方法,发送fin消息给对方。
本文为了代码上的简便,省略了很多内存释放的逻辑。
与shutdown方法相比,close方法不仅会根据当前状态决定是否要发送fin消息,还会释放该socket涉及到的一系列内存。
所以,当想要彻底关闭socket时,应该使用close方法而不是shutdown方法。
完。
本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!