Comprensión profunda de las notas de la red Linux (2): métodos de bloqueo para la cooperación entre el kernel y los procesos del usuario

Este artículo son las notas de estudio de "Comprensión profunda de las redes Linux". La versión del código fuente de Linux utilizada es 3.10. El controlador de la tarjeta de red utiliza el controlador de la tarjeta de red igb de Intel de forma predeterminada.

Lea el código fuente de Linux en línea: https://elixir.bootlin.com/linux/v3.10/source

2. Cómo coopera el kernel con los procesos del usuario (1)

1) Creación directa de socket

Desde la perspectiva de un desarrollador, llamar a la función socket crea un socket

Después de ejecutar la llamada a la función de socket, el nivel de usuario ve que se devuelve un identificador de número entero, pero en realidad el kernel crea internamente una serie de objetos del kernel relacionados con el socket. Su relación entre sí se muestra en la siguiente figura:

// net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    
    
	...
	retval = sock_create(family, type, protocol, &sock);
	...
}

sock_create es la ubicación principal para crear sockets, en la que se llama a sock_create__sock_create

// net/socket.c
int __sock_create(struct net *net, int family, int type, int protocol,
			 struct socket **res, int kern)
{
    
    
	int err;
	struct socket *sock;
	const struct net_proto_family *pf;
	...
  // 分配socket对象
	sock = sock_alloc();
	...
  // 获得每个协议族的操作表  
	pf = rcu_dereference(net_families[family]);
	...
  // 调用指定协议族的创建函数,对于AF_INET对应的是inet_create
	err = pf->create(net, sock, protocol, kern);
	...
}

Aquí __sock_create, primero llame a sock_alloc para asignar un objeto de núcleo de socket de estructura, luego obtenga la tabla de funciones de operación de la familia de protocolos y llame a su método de creación. Para la familia de protocolos AF_INET, se ejecuta el método inet_create.

// net/ipv4/af_inet.c
static struct inet_protosw inetsw_array[] =
{
    
    
	{
    
    
		.type =       SOCK_STREAM,
		.protocol =   IPPROTO_TCP,
		.prot =       &tcp_prot,
		.ops =        &inet_stream_ops,
		.no_check =   0,
		.flags =      INET_PROTOSW_PERMANENT |
			      INET_PROTOSW_ICSK,
	},
	...
};

static int inet_create(struct net *net, struct socket *sock, int protocol,
		       int kern)
{
    
    
	struct sock *sk;
	struct inet_protosw *answer;
	struct inet_sock *inet;
	struct proto *answer_prot;  
	...
	list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
    
    

		err = 0;
		if (protocol == answer->protocol) {
    
    
			if (protocol != IPPROTO_IP)
				break;
		} else {
    
    
			if (IPPROTO_IP == protocol) {
    
    
				protocol = answer->protocol;
				break;
			}
			if (IPPROTO_IP == answer->protocol)
				break;
		}
		err = -EPROTONOSUPPORT;
	}
	...
  // 将inet_stream_ops赋到socket->ops上
	sock->ops = answer->ops;
  // 获取tcp_prot
	answer_prot = answer->prot;
	...
  // 分配sock对象,并把tcp_prot赋到sock->sk_prot上
	sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
	...
	// 对sock对象进行初始化
	sock_init_data(sock, sk);
	...
}

En inet_create, busque las colecciones de implementación del método de operación inet_stream_ops y tcp_prot definidas para TCP según el tipo SOCK_STREAM, y configúrelas en socket->ops y sock->sk_prot respectivamente, como se muestra en la siguiente figura:

Más abajo vemos sock_init_data. En este método, el puntero de función sk_data_ready en el socket se inicializa y se establece en el valor predeterminado sock_def_readable, como se muestra en la siguiente figura:

// net/core/sock.c
void sock_init_data(struct socket *sock, struct sock *sk)
{
    
    
	...
	sk->sk_data_ready	=	sock_def_readable;
	sk->sk_write_space	=	sock_def_write_space;
	sk->sk_error_report	=	sock_def_error_report;
	...
}

Cuando se recibe un paquete de datos en la interrupción suave, el proceso que espera en el socket se activa llamando al puntero de función sk_data_ready (en realidad configurado en sock_def_readable()) . Esto se discutirá más adelante cuando hablemos del "módulo de interrupción suave".

En este punto, se ha creado un objeto tcp, para ser precisos, un objeto SOCKET_STREAM bajo la familia de protocolos AF_INET. Esto cuesta la sobrecarga de una llamada al sistema de socket.

2) Método de bloqueo de cooperación entre el kernel y el proceso del usuario

Modelo de bloqueo de IO:

cuando el hilo del usuario emite una solicitud de IO, el kernel verificará si los datos están listos, de lo contrario, esperará a que los datos estén listos, el hilo del usuario estará en un estado bloqueado y el usuario El hilo entregará la CPU. Cuando los datos estén listos, el kernel los copiará al hilo del usuario y devolverá el resultado al hilo del usuario, quien luego liberará el estado de bloqueo.

En el modelo IO de bloqueo síncrono, el proceso del usuario primero inicia la instrucción para crear un socket y luego cambia al estado del kernel para completar la inicialización del objeto del kernel. A continuación, Linux utiliza interrupciones duras y subprocesos ksoftirqd para procesar paquetes de datos. Una vez procesado el hilo ksoftirqd, se notificará al proceso de usuario relevante.

Desde que el proceso de usuario crea un socket hasta que un paquete de red llega a la tarjeta de red y es recibido por el proceso de usuario, el proceso general de bloqueo sincrónico de IO se muestra en la siguiente figura:

1) Esperando recibir mensajes

La función recv de la biblioteca clib ejecuta la llamada al sistema recvform. Después de ingresar a la llamada al sistema, el proceso del usuario ingresa al estado del kernel, ejecuta una serie de funciones de la capa de protocolo del kernel y luego verifica si hay datos en la cola de recepción del objeto socket, si no, se agrega a la cola de espera correspondiente. al enchufe. Finalmente, se libera la CPU y el sistema operativo seleccionará el siguiente proceso listo para ejecutar. Todo el proceso se muestra en la siguiente figura:

A continuación, veamos detalles más específicos basados ​​en el código fuente. El punto clave al que hay que prestar atención es cómo recvfrom finalmente bloquea su propio proceso.

// net/socket.c
SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
		unsigned int, flags, struct sockaddr __user *, addr,
		int __user *, addr_len)
{
    
    
	struct socket *sock;
	...
  // 根据用户传入的fd找到socket对象
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	...
	err = sock_recvmsg(sock, &msg, size, flags);
	...
}

La siguiente secuencia de llamada es: sock_recvmsg => __sock_recvmsg=>__sock_recvmsg_nosec

// net/socket.c
static inline int __sock_recvmsg_nosec(struct kiocb *iocb, struct socket *sock,
				       struct msghdr *msg, size_t size, int flags)
{
    
    
	...
	return sock->ops->recvmsg(iocb, sock, msg, size, flags);
}

Llame a recvmsg en las operaciones del objeto socket.recvmsg apunta al método inet_recvmsg.

// net/ipv4/af_inet.c
int inet_recvmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
		 size_t size, int flags)
{
    
    
	...
	err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,
				   flags & ~MSG_DONTWAIT, &addr_len);
	...
}

Aquí encontramos otro puntero de función. Esta vez llamamos al método recvmsg bajo sk_prot en el objeto socket. El método recvmsg corresponde al método tcp_recvmsg.

// net/ipv4/tcp.c
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int nonblock, int flags, int *addr_len)
{
    
    
	...
	int copied = 0;
	...
	do {
    
    
		...
		// 遍历接收队列接收数据
		skb_queue_walk(&sk->sk_receive_queue, skb) {
    
    
			...
		}
		...
		if (copied >= target) {
    
    
			release_sock(sk);
			lock_sock(sk);
		} else // 没有收到足够数据,启用sk_wait_data阻塞当前进程
			sk_wait_data(sk, &timeo);
		...
	} while (len > 0);
	...
}

Finalmente vimos lo que queríamos ver: skb_queue_walk accede a la cola de recepción debajo del objeto calcetín, como se muestra en la siguiente figura:

Si no se reciben datos o no se reciben suficientes, llame a sk_wait_data para bloquear el proceso actual.

// net/core/sock.c
int sk_wait_data(struct sock *sk, long *timeo)
{
    
    
	int rc;
	// 当前进程(current)关联到所定义的等待队列项上
	DEFINE_WAIT(wait);

 	// 调用sk_sleep获取sock对象下的wait
  // 并准备挂起,将当前进程设置为可打断(INTERRUPTIBLE)
	prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
	set_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags);
	// 通过调用schedule_timeout让出CPU,然后进行睡眠
	rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue));
	...
}

Echemos un vistazo más de cerca a cómo sk_wait_data bloquea el proceso actual, como se muestra en la siguiente figura:

Primero, en la macro DEFINE_WAIT, se define una espera de elemento de la cola de espera. En este nuevo elemento de la cola de espera, se registra la función de devolución de llamada autoremove_wake_function y el descriptor de proceso actual se asocia con sus .privatemiembros.

// include/linux/wait.h
#define DEFINE_WAIT_FUNC(name, function)				\
	wait_queue_t name = {
      
      						\
		.private	= current,				\
		.func		= function,				\
		.task_list	= LIST_HEAD_INIT((name).task_list),	\
	}

#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)

Luego llame a sk_sleep en sk_wait_data para obtener el encabezado de la lista de cola de espera wait_queue_head_t debajo del objeto socket. El código fuente de sk_sleep es el siguiente:

// include/net/sock.h
static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
    
    
	BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
	return &rcu_dereference_raw(sk->sk_wq)->wait;
}

Luego llame a prepare_to_wait para insertar el elemento de cola de espera recién definido en la cola de espera del objeto calcetín.

// kernel/wait.c
void
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
    
    
	unsigned long flags;

	wait->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&q->lock, flags);
	if (list_empty(&wait->task_list))
		__add_wait_queue(q, wait);
	set_current_state(state);
	spin_unlock_irqrestore(&q->lock, flags);
}

De esta manera, cuando el kernel recopila datos y genera un evento listo, puede buscar los elementos en espera en la cola de espera del socket y luego encontrar la función de devolución de llamada y el proceso que espera el evento listo del socket.

Finalmente, llame a sk_wait_event para liberar la CPU y el proceso entrará en estado de suspensión, lo que generará la sobrecarga de un cambio de contexto del proceso. Esta sobrecarga es costosa y requiere aproximadamente varios microsegundos de tiempo de CPU.

2)Módulo de interrupción suave

El artículo anterior habló sobre cómo la tarjeta de red recibe el paquete de red después de que llega a la tarjeta de red y finalmente se entrega a la interrupción suave para su procesamiento. Aquí, comenzando directamente desde la función de recepción tcp_v4_rcv del protocolo TCP, el total El proceso de recepción se muestra en la siguiente figura:

Después de recibir los datos en la interrupción suave (es decir, el hilo ksoftirqd en Linux), ejecutará la función tcp_v4_rcv si descubre que es un paquete TCP. A continuación, si se trata de un paquete de datos en estado ESTABLECIDO, los datos finalmente se desmontarán y se colocarán en la cola de recepción del socket correspondiente, y luego se llamará a sk_data_ready para activar el proceso del usuario.

// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
    
    
	...
	// 获取tcp header
	th = tcp_hdr(skb);
	// 获取ip header
	iph = ip_hdr(skb);
	...
	// 根据数据包header中的IP、端口信息查找到对应的socket
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
	...
	// socket未被用户锁定
	if (!sock_owned_by_user(sk)) {
    
    
		...
		{
    
    
			if (!tcp_prequeue(sk, skb))
				ret = tcp_v4_do_rcv(sk, skb);
		}
	}
	...
}

En tcp_v4_rcv, primero consulte el socket correspondiente en la máquina local según la información de origen y destino en el encabezado del paquete de red recibido. Después de encontrarlo, llame a la función tcp_v4_do_rcv

// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    
    
	...
	if (sk->sk_state == TCP_ESTABLISHED) {
    
    
		...
		// 执行连接状态下的数据处理
		if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
    
    
			rsk = sk;
			goto reset;
		}
		return 0;
	}
	// 其他非ESTABLISH状态的数据包处理
	...
}

Supongamos que estamos procesando paquetes en el estado ESTABLECIDO, por lo que ingresamos la función tcp_rcv_establecido para su procesamiento.

// net/ipv4/tcp_input.c
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
			const struct tcphdr *th, unsigned int len)
{
    
    
				...
				// 接收数据放到队列中
				eaten = tcp_queue_rcv(sk, skb, tcp_header_len,
						      &fragstolen);
			...
			// 数据准备好,唤醒socket上阻塞掉的进程
			sk->sk_data_ready(sk, 0);
			...
}

En tcp_rcv_establecido, al llamar a la función tcp_queue_rcv, los datos recibidos se colocan en la cola de recepción del socket, como se muestra en la siguiente figura:

El código fuente de la función tcp_queue_rcv es el siguiente:

// net/ipv4/tcp_input.c
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen,
		  bool *fragstolen)
{
    
    
	...
  // 把接收到的数据放到socket的接收队列的尾部  
	if (!eaten) {
    
    
		__skb_queue_tail(&sk->sk_receive_queue, skb);
		skb_set_owner_r(skb, sk);
	}
	return eaten;
}

Después de llamar a tcp_queue_rcv para recibir, llame a sk_data_ready para activar el proceso de usuario que espera en el socket . Este es nuevamente un puntero de función. En la sección anterior de "Creación directa de socket", se mencionó que la función sock_init_data ejecutada en el proceso de creación del socket configuró el puntero sk_data_ready en la función sock_def_readable. Es la función predeterminada del controlador de preparación de datos.

// net/core/sock.c
static void sock_def_readable(struct sock *sk, int len)
{
    
    
	struct socket_wq *wq;

	rcu_read_lock();
	wq = rcu_dereference(sk->sk_wq);
  // 有进程在此socket的等待队列
	if (wq_has_sleeper(wq))
    // 唤醒等待队列上的进程
		wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI |
						POLLRDNORM | POLLRDBAND);
	sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
	rcu_read_unlock();
}

En sock_def_readable, se accede nuevamente a la espera en sock->sk_wq. Al llamar a recvform en la parte anterior "esperando recibir mensajes", al final del proceso de ejecución, la DEFINE_WAIT(wait)cola de espera asociada con el proceso actual se agrega a la espera en sock->sk_wq.

El siguiente paso es llamar a wake_up_interruptible_sync_poll para reactivar el proceso que está bloqueado esperando datos en el socket, como se muestra en la siguiente figura:

// include/linux/wait.h
#define wake_up_interruptible_sync_poll(x, m)				\
	__wake_up_sync_key((x), TASK_INTERRUPTIBLE, 1, (void *) (m))
// kernel/sched/core.c
void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, void *key)
{
    
    
	unsigned long flags;
	int wake_flags = WF_SYNC;

	if (unlikely(!q))
		return;

	if (unlikely(!nr_exclusive))
		wake_flags = 0;

	spin_lock_irqsave(&q->lock, flags);
	__wake_up_common(q, mode, nr_exclusive, wake_flags, key);
	spin_unlock_irqrestore(&q->lock, flags);
}

__wake_up_commonLograr el despertar. El parámetro nr_exclusive pasado a esta llamada de función es 1. Esto significa que incluso si se bloquean varios procesos en el mismo socket, solo se despertará un proceso. Su finalidad es evitar el pánico, en lugar de despertar todos los procesos.

// kernel/sched/core.c
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
    
    
	wait_queue_t *curr, *next;

	list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
    
    
		unsigned flags = curr->flags;

		if (curr->func(curr, mode, wake_flags, key) &&
				(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
			break;
	}
}

__wake_up_commonBusque un elemento de la cola de espera en curso y luego llame a su curr->func . Cuando se ejecuta la parte anterior de la función recv "esperando recibir mensajes", cuando se usa DEFINE_WAIT() para definir el elemento de la cola de espera, el kernel establece curr->func en autoremove_wake_function

// kernel/wait.c
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    
    
	int ret = default_wake_function(wait, mode, sync, key);

	if (ret)
		list_del_init(&wait->task_list);
	return ret;
}

En autoremove_wake_function, se llama a default_wake_function

// kernel/sched/core.c
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,
			  void *key)
{
    
    
	return try_to_wake_up(curr->private, mode, wake_flags);
}

La task_struct pasada al llamar a try_to_wake_up es curr->private, que es el elemento del proceso bloqueado debido a la espera. Cuando se ejecuta esta función, el proceso que está bloqueado mientras espera en el socket será enviado a la cola ejecutable, lo que generará la sobrecarga de un cambio de contexto de proceso.

3) Resumen de bloqueo sincrónico

Todo el proceso de recepción de paquetes de red en modo de bloqueo síncrono se divide en dos partes:

  • La primera parte es el proceso donde se encuentra nuestro propio código: la función socket () que llamamos ingresará al estado del kernel para crear los objetos del kernel necesarios. La función recv () es responsable de verificar la cola de recepción después de ingresar al estado del kernel y bloquear el proceso actual para abandonar la CPU cuando no hay datos para procesar.
  • La segunda parte es la interrupción fuerte y la interrupción suave (hilo del sistema ksoftirqd). En estos componentes, una vez procesado el paquete, se colocará en la cola de recepción del socket. Luego busque el proceso que está bloqueado debido a la espera en su cola de espera de acuerdo con el objeto del kernel del socket y actívelo.

El proceso general de bloqueo sincrónico se muestra en la siguiente figura:

Cada vez, un proceso se retira de la CPU específicamente para esperar datos en un socket y luego se reemplaza con otro proceso, como se muestra en la siguiente figura. Cuando los datos están listos, el proceso inactivo se reactiva nuevamente, lo que genera un total de dos gastos generales de cambio de contexto de proceso.

Lectura recomendada:

Cinco modelos de E/S de Linux: le llevarán a comprender en profundidad los cinco modelos de E/S de Linux

Supongo que te gusta

Origin blog.csdn.net/qq_40378034/article/details/133455198
Recomendado
Clasificación