主要内容:connect()的Socket层实现、期间进程的睡眠和唤醒。

内核版本:3.15.2

我的博客:http://blog.csdn.net/zhangskd

应用层

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

Connects the socket referred to by the file descriptor sockfd to the address specified by serv_addr.

服务器端的socket会使用bind()来绑定IP和端口,客户端的socket则一般让系统自动选取IP和端口。

系统调用

connect()是由glibc提供的,声明位于include/sys/socket.h中,实现位于sysdeps/mach/hurd/connect.c中,

主要是用来从用户空间进入名为sys_socketcall的系统调用,并传递参数。sys_socketcall()实际上是所有

socket函数进入内核空间的共同入口。

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
...
switch(call) {
...
case SYS_CONNECT:
err = sys_connect(a0, (struct sockaddr __user *) a1, a[2]);
break;
...
}
return err;
}

经过了socket层的总入口sys_socketcall(),现在进入sys_connect()。

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed; /* 通过文件描述符fd,找到对应的socket实例。
* 以fd为索引从当前进程的文件描述符表files_struct实例中找到对应的file实例,
* 然后从file实例的private_data成员中获取socket实例。
*/
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out; /* 把套接字地址从用户空间拷贝到内核空间 */
err = move_addr_to_kernel(uservaddr, addrlen, &address);
if (err < 0)
goto out_put; err = security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
if (err)
goto out_put; /* 调用Socket层的操作函数,如果是SOCK_STREAM,则proto_ops为inet_stream_ops,
* 函数指针指向inet_stream_connect()。
*/
err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags); out_put:
fput_light(sock->file, fput_needed); /* 减少套接口文件的引用计数 */ out:
return err;
}

Socket层

SOCK_STREAM套接口的socket层操作函数集实例为inet_stream_ops,其中主动建立连接的函数

为inet_stream_connect()。

const struct proto_ops inet_stream_ops = {
.family = PF_INET,
.owner = THIS_MODULE,
....
.connect = inet_stream_connect,
...
};
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags)
{
int err; lock_sock(sock->sk);
err = __inet_stream_connect(sock, uaddr,addr_len, flags);
release_sock(sock->sk);
return err;
}

__inet_stream_connect()主要做了以下事情:

1. 检查socket地址长度和使用的协议族。

2. 检查socket的状态,必须是SS_UNCONNECTED或SS_CONNECTING。

3. 调用tcp_v4_connect()来发送SYN包。

4. 等待后续握手的完成:

如果socket是非阻塞的,那么就直接返回错误码-EINPROGRESS。

如果socket为阻塞的,就调用inet_wait_for_connect(),通过睡眠来等待。在以下三种情况下会被唤醒:

(1) 使用SO_SNDTIMEO选项时,睡眠时间超过设定值,返回0。connect()返回错误码-EINPROGRESS。

(2)  收到信号,返回剩余的等待时间。connect()返回错误码-ERESTARTSYS或-EINTR。

(3) 三次握手成功,sock的状态从TCP_SYN_SENT或TCP_SYN_RECV变为TCP_ESTABLISHED,

sock I/O事件的状态变化处理函数sock_def_wakeup()就会唤醒进程。connect()返回0。

/* Connect to a remote host. There is regrettably still a little TCP magic in here. */
int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags)
{
struct sock *sk = sock->sk;
int err;
long timeo; /* socket地址长度错误 */
if (addr_len < sizeof(uaddr->sa_family))
return -EINVAL; /* socket的协议族错误 */
if (uaddr->sa_family == AF_UNSPEC) {
/* 如果使用的是TCP,则sk_prot为tcp_prot,disconnect为tcp_disconnect() */
err = sk->sk_prot->disconnect(sk, flags); /* 根据是否成功断开连接,来设置socket状态 */
sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
goto out;
} switch(sock->state) {
default:
err = -EINVAL; /* Invalid argument */
goto out; case SS_CONNECTED: /* 此套接口已经和对端的套接口相连接了,即连接已经建立 */
err = -EISCONN; /* Transport endpoint is already connected */
goto out; case SS_CONNECTING: /* 此套接口正在尝试连接对端的套接口,即连接正在建立中 */
err = -EALREADY; /* Operation already in progress */
/* Fall out of switch with err, set for this state */
break; case SS_UNCONNECTED: /* 此套接口尚未连接对端的套接口,即连接尚未建立 */
err = -EISCONN; if (sk->sk_state != TCP_CLOSE)
goto out; /* 如果使用的是TCP,则sk_prot为tcp_prot,connect为tcp_v4_connect() */
err = sk->sk_prot->connect(sk, uaddr, addr_len); /* 发送SYN包 */
if (err < 0)
goto out; /* 发出SYN包后socket状态设为正在连接 */
sock->state = SS_CONNECTING; /* Just entered SS_CONNECTING state; the only difference
* is that return value in non-blocking case is EINPROGRESS,
* rather than EALREADY.
*/
err = -EINPROGRESS; /* Operation now in progress */
break;
} /* sock的发送超时时间,非阻塞则为0 */
timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); /* 发出SYN包后,等待后续握手的完成 */
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) { int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
tcp_sk(sk)->fastopen_req &&
tcp_sk(sk)->fastopen_req->data ? 1 : 0; /* 如果是非阻塞的,那么就直接返回错误码-EINPROGRESS。
* socket为阻塞时,使用inet_wait_for_connect()来等待协议栈的处理:
* 1. 使用SO_SNDTIMEO,睡眠时间超过timeo就返回0,之后返回错误码-EINPROGRESS。
* 2. 收到信号,就返回剩余的等待时间。之后会返回错误码-ERESTARTSYS或-EINTR。
* 3. 三次握手成功,被sock I/O事件处理函数唤醒,之后会返回0。
*/
if (! timeo || ! inet_wait_for_connect(sk,timeo, writebias))
goto out; err = sock_intr_errno(timeo);
/* 进程收到信号,如果err为-ERESTARTSYS,接下来库函数会重新调用connect() */
if (signal_pending(current))
goto out;
} /* Connection was closed by RST, timeout, ICMP error or
* another process disconnected us.
*/
if (sk->sk_state == TCP_CLOSE)
goto sock_error; sock->state = SS_CONNECTED; /* 更新socket状态为连接已建立 */
err = 0; /* 清除错误码 */ out:
return err; sock_error:
err = sock_error(sk) ?: -ECONNABORTED;
sock->state = SS_UNCONNECTED; /* 如果使用的是TCP,则sk_prot为tcp_prot,disconnect为tcp_disconnect() */
if (sk->sk_prot->disconnect(sk, flags)) /* 如果失败 */
sock->state == SS_DISCONNECTING; goto out;
} static inline long sock_sndtimeo(const struct sock *sk, bool noblock)
{
return noblock ? 0 : sk->sk_sndtimeo;
} static inline int sock_intr_errno(long timeo)
{
return timeo == MAX_SCHEDULE_TIMEOUT ? -ERESTARTSYS : -EINTR;
}

进程的睡眠

connect()的超时时间为sk->sk_sndtimeo,在sock_init_data()中初始化为MAX_SCHEDULE_TIMEOUT,

表示无限等待,可以通过SO_SNDTIMEO选项来修改。

static long inet_wait_for_connect(struct sock *sk, long timeo, int writebias)
{
DEFINE_WAIT(wait); /* 初始化等待任务 */ /* 把等待任务加入到socket的等待队列头部,把进程的状态设为TASK_INTERRUPTIBLE */
prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
sk->sk_write_pending += writebias; /* Basic assumption: if someone sets sk->sk_err, he _must_ change state of the socket
* from TCP_SYN_*. Connect() does not allow to get error notifications without closing
* the socket.
*/ /* 完成三次握手后,状态就会变为TCP_ESTABLISHED,从而退出循环 */
while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
release_sock(sk); /* 等下要睡觉了,先释放锁 */ /* 进入睡眠,直到超时或收到信号,或者被I/O事件处理函数唤醒。
* 1. 如果是收到信号退出的,timeo为剩余的jiffies。
* 2. 如果使用了SO_SNDTIMEO选项,超时退出后,timeo为0。
* 3. 如果没有使用SO_SNDTIMEO选项,timeo为无穷大,即MAX_SCHEDULE_TIMEOUT,
* 那么返回值也是这个,而超时时间不定。为了无限阻塞,需要上面的while循环。
*/
timeo = schedule_timeout(timeo); lock_sock(sk); /* 被唤醒后重新上锁 */ /* 如果进程有待处理的信号,或者睡眠超时了,退出循环,之后会返回错误码 */
if (signal_pending(current) || !timeo)
break; /* 继续睡眠吧 */
prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
} /* 等待结束时,把等待进程从等待队列中删除,把当前进程的状态设为TASK_RUNNING */
finish_wait(sk_sleep(sk), &wait);
sk->sk_write_pending -= writebias;
return timeo;
}

进程的唤醒

三次握手中,当客户端收到SYNACK、发出ACK后,连接就成功建立了。

此时连接的状态从TCP_SYN_SENT或TCP_SYN_RECV变为TCP_ESTABLISHED,sock的状态发生变化,

会调用sock_def_wakeup()来处理连接状态变化事件,唤醒进程,connect()就能成功返回了。

sock_def_wakeup()的函数调用路径如下:

tcp_v4_rcv

tcp_v4_do_rcv

tcp_rcv_state_process

tcp_rcv_synsent_state_process

tcp_finish_connect

sock_def_wakeup

wake_up_interruptible_all

__wake_up

__wake_up_common

void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
...
tcp_set_state(sk, TCP_ESTABLISHED); /* 在这里设置为连接已建立的状态 */
...
if (! sock_flag(sk, SOCK_DEAD)) {
sk->sk_state_change(sk); /* 指向sock_def_wakeup,会唤醒调用connect()的进程,完成连接的建立 */
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT); /* 如果使用了异步通知,则发送SIGIO通知进程可写 */
}
}
static void sock_def_wakeup(struct sock *sk)
{
struct socket_wq *wq; /* socket的等待队列和异步通知队列 */ rcu_read_lock();
wq = rcu_dereference(sk->sk_wq); if (wq_has_sleeper(wq)) /* 有进程阻塞在此socket上 */
wake_up_interruptible_all(&wq->wait); /* 唤醒此socket上的所有睡眠进程 */ rcu_read_unlock();
}

最终调用__wake_up_common(),由于nr_exclusive为0,会把此socket上所有的等待进程都唤醒。

Socket层实现系列 — connect()的实现的更多相关文章

  1. Socket层实现系列 — send()类发送函数的实现

    主要内容:socket发送函数的系统调用.Socket层实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 发送流程图 以下是send().sendt ...

  2. Socket层实现系列 — 睡眠驱动的同步等待

    主要内容:Socket的同步等待机制,connect和accept等待的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 概述 socket上定义了 ...

  3. Socket层实现系列 — getsockname()和getpeername()的实现

    本文主要介绍了getsockname()和getpeername()的内核实现. 内核版本:3.6 Author:zhangskd @ csdn blog 应用层 int getsockname(in ...

  4. Socket层实现系列 — 信号驱动的异步等待

    主要内容:Socket的异步通知机制. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 概述 socket上定义了几个IO事件:状态改变事件.有数据可读事 ...

  5. Socket层实现系列 — accept()的实现(一)

    本文主要介绍了accept()的系统调用.Socket层实现,以及TCP层实现. 内核版本:3.6 Author:zhangskd @ csdn blog 应用层 int accept(int soc ...

  6. Socket层实现系列 — listen()的实现

    本文主要分析listen()的内核实现,包括它的系统调用.Socket层实现.半连接队列,以及监听哈希表. 内核版本:3.6 Author:zhangskd @ csdn blog 应用层 int l ...

  7. Socket层实现系列 — bind()的实现(一)

    bind()函数的使用方法很简单,但是它是怎么实现的呢? 笔者从应用层出发,沿着网络协议栈,分析了bind()的系统调用.Socket层实现,以及它的TCP层实现. 本文主要内容:bind()的系统调 ...

  8. Socket层实现系列 — I/O事件及其处理函数

    主要内容:Socket I/O事件的定义.I/O处理函数的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd I/O事件定义 sock中定义了几个I/ ...

  9. Socket层实现系列 — bind()的实现(二)

    本文主要内容:bind()的TCP层实现.端口的冲突处理,以及不同内核版本的实现差异. 内核版本:3.6 Author:zhangskd @ csdn blog TCP层实现 SOCK_STREAM套 ...

随机推荐

  1. List Set Map比较

    List按对象进入的顺序保存对象,不做排序或编辑操作. Set对每个对象只接受一次,并使用自己内部的排序方法(通常,你只关心某个元素是否属于Set,而不关心它的顺序–否则应该使用List). Map同 ...

  2. Go 语言教程

    Go 语言教程 Go 是一个开源的编程语言,它能让构造简单.可靠且高效的软件变得容易. Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发, ...

  3. day08 JSP

    day08 JSP 1. jsp 入门和 jsp 运行原理 2. jsp 语法 2.1 jsp 模板元素:jsp 页面中的 html 内容.它定义了网络基本骨架,即定义了页面结构和外观. 2.2 js ...

  4. CSDN没有审核投诉的真实性直接删除博主上传的资源

      今天打开博客,发现一条未读通知:您上传的资源* * * *因质量投诉没有通过审核,如有疑问,请联系webmaster@csdn.net 我马上去看了下我的资源下载页,资源已经被删除,积分也已清空- ...

  5. Python 2.7 闭包的局限

    想法源自:http://stackoverflow.com/questions/141642/what-limitations-have-closures-in-python-compared-to- ...

  6. 负载均衡LVS(DR模式)安装实战

    1.编译安装ipvsadm 首先从LVS官网下载tarball,解压后make && make install即可. 要注意的是LVS的依赖有:popt-static.libnl.ke ...

  7. rbac数据库设计

    1 rbac数据库设计 RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制分享牛原创,分享牛系列,分享牛.rbac 用户角色权限资源表如 ...

  8. clang-format中文出错

    clang-format中文出错(金庆的专栏)VS2015 Community + clang-format(Visual Studio plugin installer, based on SVN ...

  9. PHP学习(3)—在HTML中嵌入PHP

    我们以一个提交订单和显示订单信息的例子为学习PHP的开始.这个例子包含两个文件.一个提交订单的html文件:orderform.html,一个显示订单信息的php文件:processorder.php ...

  10. Linux 高性能服务器编程——多线程编程

    问题聚焦:     在简单地介绍线程的基本知识之后,主要讨论三个方面的内容:    1 创建线程和结束线程:    2 读取和设置线程属性:    3 线程同步方式:POSIX信号量,互斥锁和条件变量 ...