目录

1 netlink 通信数据结构

1.1 netlink 消息报头:struct nlmsghdr

1.2 socket 消息数据包结构:struct msghdr

1.3 netlink消息处理宏

2 应用层向内核发送netlink消息

3 内核向应用层发送netlink消息

4 应用层接收内核netlink消息


1 netlink 通信数据结构

1.1 netlink 消息报头:struct nlmsghdr

struct nlmsghdr {__u32		nlmsg_len;	/* Length of message including header */__u16		nlmsg_type;	/* Message content */__u16		nlmsg_flags;	/* Additional flags */__u32		nlmsg_seq;	/* Sequence number */__u32		nlmsg_pid;	/* Sending process port ID */
};

netlink消息同TCP/UDP消息一样,也需要遵循协议要求的格式,每个netlink消息的开头是固定长度的netlink报头,报头后才是实际的载荷。netlink报头一共占16个字节,具体内容即同struct nlmsghdr中定义的一样。
nlmsg_len:整个netlink消息的长度(包含消息头);
nlmsg_type:消息状态,内核在include/uapi/linux/netlink.h中定义了以下4种通用的消息类型,它们分别是:

  • NLMSG_NOOP:不执行任何动作,必须将该消息丢弃;
  • NLMSG_ERROR:消息发生错误;
  • NLMSG_DONE:标识分组消息的末尾;
  • NLMSG_OVERRUN:缓冲区溢出,表示某些消息已经丢失。

除了这4种类型的消息以外,不同的netlink协议也可以自行添加自己所特有的消息类型,但是内核定义了类型保留宏(#define NLMSG_MIN_TYPE 0x10),即小于该值的消息类型值由内核保留,不可用。

nlmsg_flags:消息标记,它们用以表示消息的类型,同样定义在include/uapi/linux/netlink.h中;

#define NLM_F_REQUEST		1	/* It is request message. 	*/
#define NLM_F_MULTI		2	/* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK		4	/* Reply with ack, with zero or error code */
#define NLM_F_ECHO		8	/* Echo this request 		*/
#define NLM_F_DUMP_INTR		16	/* Dump was inconsistent due to sequence change *//* Modifiers to GET request */
#define NLM_F_ROOT	0x100	/* specify tree	root	*/
#define NLM_F_MATCH	0x200	/* return all matching	*/
#define NLM_F_ATOMIC	0x400	/* atomic GET		*/
#define NLM_F_DUMP	(NLM_F_ROOT|NLM_F_MATCH)/* Modifiers to NEW request */
#define NLM_F_REPLACE	0x100	/* Override existing		*/
#define NLM_F_EXCL	0x200	/* Do not touch, if it exists	*/
#define NLM_F_CREATE	0x400	/* Create, if it does not exist	*/
#define NLM_F_APPEND	0x800	/* Add to end of list		*/

nlmsg_seq:消息序列号,用以将消息排队,有些类似TCP协议中的序号(不完全一样),但是netlink的这个字段是可选的,不强制使用;

nlmsg_pid:发送端口的ID号,对于内核来说该值就是0,对于用户进程来说就是其socket所绑定的ID号。

1.2 socket 消息数据包结构:struct msghdr

struct user_msghdr {void		__user *msg_name;	/* ptr to socket address structure */int		msg_namelen;		/* size of socket address structure */struct iovec	__user *msg_iov;	/* scatter/gather array */__kernel_size_t	msg_iovlen;		/* # elements in msg_iov */void		__user *msg_control;	/* ancillary data */__kernel_size_t	msg_controllen;		/* ancillary data buffer length */unsigned int	msg_flags;		/* flags on received message */
};

应用层向内核传递消息可以使用sendto()或sendmsg()函数,其中sendmsg函数需要应用程序手动封装msghdr消息结构,而sendto()函数则会由内核代为分配。其中
msg_name:指向数据包的目的地址;
msg_namelen:目的地址数据结构的长度;
msg_iov:消息包的实际数据块,定义如下:

struct iovec
{void *iov_base;	/* BSD uses caddr_t (1003.1g requires void *) */__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};//iov_base:消息包实际载荷的首地址;
//iov_len:消息实际载荷的长度。

msg_control:消息的辅助数据;
msg_controllen:消息辅助数据的大小;
msg_flags:接收消息的标识。

对于该结构,我们更需要关注的是前三个变量参数,对于netlink数据包来说其中msg_name指向的就是目的sockaddr_nl地址结构实例的首地址,iov_base指向的就是消息实体中的nlmsghdr消息头的地址,而iov_len赋值为nlmsghdr中的nlmsg_len即可(消息头+实际数据)。

1.3 netlink消息处理宏

#define NLMSG_ALIGNTO	4U
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )			/* 对len执行4字节对齐 */
#define NLMSG_HDRLEN	 ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))				/* netlink消息头长度 */
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)									/* netlink消息载荷len加上消息头 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))								/* 对netlink消息全长执行字节对齐 */
#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))					/* 获取netlink消息实际载荷位置 */
#define NLMSG_NEXT(nlh,len)	 ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))/* 取得下一个消息的首地址,同时len也减少为剩余消息的总长度 */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \(nlh)->nlmsg_len <= (len))											/* 验证消息的长度 */
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))				/* 返回PAYLOAD的长度 */

Linux为了处理netlink消息方便,在 include/uapi/linux/netlink.h中定义了以上消息处理宏,用于各种场合。对于Netlink消息来说,处理如下格式(见netlink.h):

/* ========================================================================*         Netlink Messages and Attributes Interface (As Seen On TV)* ------------------------------------------------------------------------*                          Messages Interface* ------------------------------------------------------------------------** Message Format:*    <--- nlmsg_total_size(payload)  --->*    <-- nlmsg_msg_size(payload) ->*   +----------+- - -+-------------+- - -+-------- - -*   | nlmsghdr | Pad |   Payload   | Pad | nlmsghdr*   +----------+- - -+-------------+- - -+-------- - -*   nlmsg_data(nlh)---^                   ^*   nlmsg_next(nlh)-----------------------+** Payload Format:*    <---------------------- nlmsg_len(nlh) --------------------->*    <------ hdrlen ------>       <- nlmsg_attrlen(nlh, hdrlen) ->*   +----------------------+- - -+--------------------------------+*   |     Family Header    | Pad |           Attributes           |*   +----------------------+- - -+--------------------------------+*   nlmsg_attrdata(nlh, hdrlen)---^** ------------------------------------------------------------------------*                          Attributes Interface* ------------------------------------------------------------------------** Attribute Format:*    <------- nla_total_size(payload) ------->*    <---- nla_attr_size(payload) ----->*   +----------+- - -+- - - - - - - - - +- - -+-------- - -*   |  Header  | Pad |     Payload      | Pad |  Header*   +----------+- - -+- - - - - - - - - +- - -+-------- - -*                     <- nla_len(nla) ->      ^*   nla_data(nla)----^                        |*   nla_next(nla)-----------------------------'**=========================================================================*/

2 应用层向内核发送netlink消息

使用如下示例程序可向内核netlink套接字发送消息:

#define TEST_DATA_LEN	16
#DEFINE TEST_DATA		"netlink send test"		/* 仅作为示例,内核NETLINK_ROUTE套接字无法解析 */struct sockaddr_nl nladdr;
struct msghdr msg;
struct nlmsghdr *nlhdr;
struct iovec iov;/* 填充目的地址结构 */
memset(&nladdr, 0, sizeof(nladdr));
nladdr.nl_family = AF_NETLINK;
nladdr.nl_pid = 0;						/* 地址为内核 */
nladdr.nl_groups = 0;						/* 单播 *//* 填充netlink消息头 */
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(TEST_DATA_LEN));nlhdr->nlmsg_len = NLMSG_LENGTH(TEST_DATA_LEN);
nlhdr->nlmsg_flags = NLM_F_REQUEST;
nlhdr->nlmsg_pid = get_pid();					/* 当前套接字所绑定的ID号(此处为本进程的PID) */
nlhdr->nlmsg_seq = 0;/* 填充netlink消息实际载荷 */
strcpy(NLMSG_DATA(nlhdr), TEST_DATA);
iov.iov_base = (void *)nlhdr;
iov.iov_len = nlhdr->nlmsg_len;/* 填充数据消息结构 */
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;/* 发送netlink消息 */
sendmsg (sock, &msg, 0); /* sock描述符见《Netlink 内核实现分析(一):创建》,为NETLINK_ROUTE类型套接字 */

这里列出了一个调用sendmsg向内核发送消息的示例代码片段(仅作为示例,发送的消息内核netlink套接字可能无法解析)。首先初始化目的地址数据结构,设置nl_pid和nl_groups为0指定消息的目的地址为内核;然后初始化netlink消息头指明消息的长度为TEST_DATA_LEN + NLMSG_ALIGN(sizeof(struct nlmsghdr))(包含消息头),发送端的ID号为发送socket消息所绑定的ID号(这样内核才知道消息是谁发送的);然后设置消息的实际载荷,将数据拷贝到紧接消息头后的实际载荷部分;最后组装成msg消息就可以调用sendmsg向内核发送了。

下面跟随内核的sendmsg系统调用的整个流程来分析消息是如何被送到内核的(需要注意的是,在不使用NETLINK_MMAP技术的情况下,整个发送的过程中存在1~2次数据的内存拷贝动作,后面会逐一点出!):

                                       

                                                图1 用户态netlink数据发送流程

SYSCALL_DEFINE3(sendmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags)
{if (flags & MSG_CMSG_COMPAT)return -EINVAL;return __sys_sendmsg(fd, msg, flags);
}long __sys_sendmsg(int fd, struct user_msghdr __user *msg, unsigned flags)
{int fput_needed, err;struct msghdr msg_sys;struct socket *sock;sock = sockfd_lookup_light(fd, &err, &fput_needed);if (!sock)goto out;err = ___sys_sendmsg(sock, msg, &msg_sys, flags, NULL);fput_light(sock->file, fput_needed);
out:return err;
}

sendmsg系统调用调用__sys_sendmsg来进行实际的操作,这里首先通过fd描述符找到对应的socket套接字结构实例,然后调用___sys_sendmsg()函数,传入的参数中第三个和最后一个需要关注一下,其中第三个它是一个内核版的socket消息数据包结构,同应用层的略有不同,定义如下:

struct msghdr {void		*msg_name;	/* ptr to socket address structure */int		msg_namelen;	/* size of socket address structure */struct iov_iter	msg_iter;	/* data */void		*msg_control;	/* ancillary data */__kernel_size_t	msg_controllen;	/* ancillary data buffer length */unsigned int	msg_flags;	/* flags on received message */struct kiocb	*msg_iocb;	/* ptr to iocb for async requests */
};

其中msg_name、msg_namelen、msg_control、msg_controllen和msg_flags字段同应用层的含义是一样的,msg_iter为msg_iov和msg_iovlen的合体,最后msg_iocb用于异步请求。最后一个参数是一个struct used_address结构体指针,这个结构体定义如下:

struct used_address {struct sockaddr_storage name;unsigned int name_len;
};

这里的name字段用来存储消息的地址,name_len字段是消息地址的长度,它们同struct msghdr结构体的前两个字段一致。该结构体主要用与sendmmsg系统调用(用于同事时向一个socket地址发送多个数据包,可以避免重复的网络security检查,从而提高发送效率)保存多个数据包的目的地址。现在这里设置为NULL,表示不使用。继续往下分析,进入___sys_sendmsg()函数内部,这个函数比较长,分段来分析:

static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg,struct msghdr *msg_sys, unsigned int flags,struct used_address *used_address)
{struct compat_msghdr __user *msg_compat =(struct compat_msghdr __user *)msg;struct sockaddr_storage address;struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;unsigned char ctl[sizeof(struct cmsghdr) + 20]__attribute__ ((aligned(sizeof(__kernel_size_t))));/* 20 is size of ipv6_pktinfo */unsigned char *ctl_buf = ctl;int ctl_len;ssize_t err;msg_sys->msg_name = &address;if (MSG_CMSG_COMPAT & flags)err = get_compat_msghdr(msg_sys, msg_compat, NULL, &iov);elseerr = copy_msghdr_from_user(msg_sys, msg, NULL, &iov);if (err < 0)return err;

这里的iovstack数组是用来加速用户数据拷贝的(这里假定用户数据的iovec个数通常不会超过UIO_FASTIOV个,如果超过会通过kmalloc分配内存)。首先这里判断flag中是否设置了32bit修正标识,从前文中系统调用的入口处已经可以看出了,这里显然不会设置该标识位,所以这里调用copy_msghdr_from_user函数将用户空间传入的消息(struct user_msghdr __user *msg)安全的拷贝到内核空间中(struct msghdr *msg_sys),来简单的看一下这个函数:
 

static int copy_msghdr_from_user(struct msghdr *kmsg,struct user_msghdr __user *umsg,struct sockaddr __user **save_addr,struct iovec **iov)
{struct sockaddr __user *uaddr;struct iovec __user *uiov;size_t nr_segs;ssize_t err;if (!access_ok(VERIFY_READ, umsg, sizeof(*umsg)) ||__get_user(uaddr, &umsg->msg_name) ||__get_user(kmsg->msg_namelen, &umsg->msg_namelen) ||__get_user(uiov, &umsg->msg_iov) ||__get_user(nr_segs, &umsg->msg_iovlen) ||__get_user(kmsg->msg_control, &umsg->msg_control) ||__get_user(kmsg->msg_controllen, &umsg->msg_controllen) ||__get_user(kmsg->msg_flags, &umsg->msg_flags))return -EFAULT;if (!uaddr)kmsg->msg_namelen = 0;if (kmsg->msg_namelen < 0)return -EINVAL;if (kmsg->msg_namelen > sizeof(struct sockaddr_storage))kmsg->msg_namelen = sizeof(struct sockaddr_storage);if (save_addr)*save_addr = uaddr;if (uaddr && kmsg->msg_namelen) {if (!save_addr) {err = move_addr_to_kernel(uaddr, kmsg->msg_namelen,kmsg->msg_name);if (err < 0)return err;}} else {kmsg->msg_name = NULL;kmsg->msg_namelen = 0;}if (nr_segs > UIO_MAXIOV)return -EMSGSIZE;kmsg->msg_iocb = NULL;return import_iovec(save_addr ? READ : WRITE, uiov, nr_segs,UIO_FASTIOV, iov, &kmsg->msg_iter);
}

函数首先调用access_ok检查用户数据的有效性,然后调用__get_user函数执行单数据的复制操作(并没有复制数据包内容),接着做一些简单的入参判断。然后如果用户消息中存在目的地址且入参save_addr为空(当前情景中正好就是这类情况),就调用move_addr_to_kernel()函数将消息地址拷贝到内核kmsg的结构中,否则将kmsg中的目的地址和长度字段置位空。接下来判断消息实际载荷iovec结构的个数,这里UIO_MAXIOV值定义为1024,也就是说消息数据iovec结构的最大个数不能超过这个值,这点非常重要。
最后调用import_iovec()函数开始执行实际数据从用户态向内核态的拷贝动作(注意这里并没有拷贝用户空间实际消息载荷数据,仅仅检查了用户地址有效性并拷贝了长度等字段),在拷贝完成后,&kmsg->msg_iter中的数据初始化情况如下:
int type:WRITE;
size_t iov_offset:初始化为0;
size_t count:所有iovec结构数据的总长度(即iov->iov_len的总和);
const struct iovec *iov:首个iov结构指针;
unsigned long nr_segs:iovec结构的个数。
数据拷贝完成后,再回到___sys_sendmsg函数中继续分析:

	err = -ENOBUFS;if (msg_sys->msg_controllen > INT_MAX)goto out_freeiov;ctl_len = msg_sys->msg_controllen;if ((MSG_CMSG_COMPAT & flags) && ctl_len) {err =cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl,sizeof(ctl));if (err)goto out_freeiov;ctl_buf = msg_sys->msg_control;ctl_len = msg_sys->msg_controllen;} else if (ctl_len) {if (ctl_len > sizeof(ctl)) {ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);if (ctl_buf == NULL)goto out_freeiov;}err = -EFAULT;/** Careful! Before this, msg_sys->msg_control contains a user pointer.* Afterwards, it will be a kernel pointer. Thus the compiler-assisted* checking falls down on this.*/if (copy_from_user(ctl_buf,(void __user __force *)msg_sys->msg_control,ctl_len))goto out_freectl;msg_sys->msg_control = ctl_buf;}

这一段程序是用来拷贝消息辅助数据的,比较直观,我们前文中的示例程序并没有传递辅助数据,所以这里不详细分析,继续往下看:

	msg_sys->msg_flags = flags;if (sock->file->f_flags & O_NONBLOCK)msg_sys->msg_flags |= MSG_DONTWAIT;/** If this is sendmmsg() and current destination address is same as* previously succeeded address, omit asking LSM's decision.* used_address->name_len is initialized to UINT_MAX so that the first* destination address never matches.*/if (used_address && msg_sys->msg_name &&used_address->name_len == msg_sys->msg_namelen &&!memcmp(&used_address->name, msg_sys->msg_name,used_address->name_len)) {err = sock_sendmsg_nosec(sock, msg_sys);goto out_freectl;}err = sock_sendmsg(sock, msg_sys);/** If this is sendmmsg() and sending to current destination address was* successful, remember it.*/if (used_address && err >= 0) {used_address->name_len = msg_sys->msg_namelen;if (msg_sys->msg_name)memcpy(&used_address->name, msg_sys->msg_name,used_address->name_len);}

这里首先保存用户传递的flag标识,然后判断如果当前的socket已经被配置为非阻塞模式则置位MSG_DONTWAIT标识(定义在include/linux/socket.h中)。接下来通过传入的used_address指针判断当前发送消息的目的地址是否同它记录的一致,如果一致则调用sock_sendmsg_nosec()函数发送数据,否则调用sock_sendmsg()函数发送数据,sock_sendmsg()其实最终也是通过调用sock_sendmsg_nosec()来发送数据的,它们的区别就在于是否调用安全检查函数,如下:

int sock_sendmsg(struct socket *sock, struct msghdr *msg)
{int err = security_socket_sendmsg(sock, msg,msg_data_left(msg));return err ?: sock_sendmsg_nosec(sock, msg);
}
EXPORT_SYMBOL(sock_sendmsg);

在sendmmsg系统调用每一次发送多个消息时,由于发送的目的地一般都是一致的,所以只需要在发送第一个消息爆时执行检查就可以了,通过这种策略就可以加速数据的发送。最后,在发送完数据后,如果传入的used_address指针非空,就会将本次成功发送数据的目的地址记录下来,供下次发送数据比较。接下来进入sock_sendmsg_nosec(sock, msg_sys)内部继续分析消息的发送流程:

static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));BUG_ON(ret == -EIOCBQUEUED);return ret;
}

这里调用了socket所绑定协议特有的数据发送钩子函数,其中最后一个参数为msg->msg_iter->count,即消息实际载荷的总长度。在前一篇文章中已经看到了对于netlink类型的套接字来说该函数被注册为netlink_sendmsg(),下面来分析这个函数,这个函数较长,分段分析:

static int netlink_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{struct sock *sk = sock->sk;struct netlink_sock *nlk = nlk_sk(sk);DECLARE_SOCKADDR(struct sockaddr_nl *, addr, msg->msg_name);u32 dst_portid;u32 dst_group;struct sk_buff *skb;int err;struct scm_cookie scm;u32 netlink_skb_flags = 0;if (msg->msg_flags&MSG_OOB)return -EOPNOTSUPP;err = scm_send(sock, msg, &scm, true);if (err < 0)return err;

首先,这里定义了一个struct sockaddr_nl *addr指针,它指向了msg->msg_name表示消息的目的地址(会做地址长度检查);然后调用scm_send()发送消息辅助数据(不分析)。

	if (msg->msg_namelen) {err = -EINVAL;if (addr->nl_family != AF_NETLINK)goto out;dst_portid = addr->nl_pid;dst_group = ffs(addr->nl_groups);err =  -EPERM;if ((dst_group || dst_portid) &&!netlink_allowed(sock, NL_CFG_F_NONROOT_SEND))goto out;netlink_skb_flags |= NETLINK_SKB_DST;} else {dst_portid = nlk->dst_portid;dst_group = nlk->dst_group;}

这里如果用户指定了netlink消息的目的地址,则对其进行校验,然后判断当前netlink协议的NL_CFG_F_NONROOT_SEND标识是否设置,如果设置了改标识则允许非root用户发送组播,对于NETLINK_ROUTE类型的netlink套接字,并没有设置该标识,表明非root用户不能发送组播消息;然后设置NETLINK_SKB_DST标识。如果用户没有指定netlink消息的目的地址,则使用netlink套接字默认的(该值默认为0,会在调用connect系统调用时在netlink_connect()中被赋值为用户设置的值)。注意这里dst_group经过ffs的处理后转化为组播地址位数(找到最低有效位)。

	if (!nlk->bound) {err = netlink_autobind(sock);if (err)goto out;} else {/* Ensure nlk is hashed and visible. */smp_rmb();}

接下来判断当前的netlink套接字是否被绑定过,如果没有绑定过这里调用netlink_autobind()进行动态绑定,该函数在前一篇文章中已经分析,继续往下分析

	/* It's a really convoluted way for userland to ask for mmaped* sendmsg(), but that's what we've got...*/if (netlink_tx_is_mmaped(sk) &&msg->msg_iter.type == ITER_IOVEC &&msg->msg_iter.nr_segs == 1 &&msg->msg_iter.iov->iov_base == NULL) {err = netlink_mmap_sendmsg(sk, msg, dst_portid, dst_group,&scm);goto out;}

如果内核配置了CONFIG_NETLINK_MMAP内核选项,则表示内核空间和应用层的消息发送队列支持内存映射,然后通过调用netlink_mmap_sendmsg来发送netlink消息,该种方式将减少数据的内存数据的拷贝动作,减少发送时间和资源占用。现我的环境中并不支持,继续往下分析:

	err = -EMSGSIZE;if (len > sk->sk_sndbuf - 32)goto out;err = -ENOBUFS;skb = netlink_alloc_large_skb(len, dst_group);if (skb == NULL)goto out;

接下来判断需要发送的数据是否过长(长于发送缓存大小),然后通过netlink_alloc_large_skb分配skb结构(传入的参数为消息载荷的长度以及组播地址)。

	NETLINK_CB(skb).portid	= nlk->portid;NETLINK_CB(skb).dst_group = dst_group;NETLINK_CB(skb).creds	= scm.creds;NETLINK_CB(skb).flags	= netlink_skb_flags;err = -EFAULT;if (memcpy_from_msg(skb_put(skb, len), msg, len)) {kfree_skb(skb);goto out;}err = security_netlink_send(sk, skb);if (err) {kfree_skb(skb);goto out;}if (dst_group) {atomic_inc(&skb->users);netlink_broadcast(sk, skb, dst_portid, dst_group, GFP_KERNEL);}err = netlink_unicast(sk, skb, dst_portid, msg->msg_flags&MSG_DONTWAIT);

在成功创建skb结构之后,这里就开始初始化它,这里使用到了skb中的扩展cb字段(char cb[48] __aligned(8),一共48个字节用于存放netlink的地址和标识相关的附加信息足够了),同时使用宏NETLINK_CB来操作这些字段。netlink将skb的cb字段强制定义为struct netlink_skb_parms结构:

struct netlink_skb_parms {struct scm_creds	creds;		/* Skb credentials	*/__u32			portid;__u32			dst_group;__u32			flags;struct sock		*sk;
};

其中portid表示原端套接字所绑定的id,dst_group表示消息目的组播地址,flag为标识,sk指向原端套接字的sock结构。
这里首先将套接字绑定的portid赋值到skb得cb字段中、同时设置组播地址的数量以及netlink_skb标识(这里是已经置位NETLINK_SKB_DST)。接下来调用最关键的调用memcpy_from_msg拷贝数据,它首先调用skb_put调整skb->tail指针,然后执行copy_from_iter(data, len, &msg->msg_iter)将数据从msg->msg_iter中传输到skb->data中(这是第一次内存拷贝动作!将用户空间数据直接拷贝到内核skb中)。
接下来调用security_netlink_send()执行security检查,最后如果是组播发送则调用netlink_broadcast()发送消息,否则调用netlink_unicast()发送单播消息。我们先跟随当前的情景来分析单播发送函数netlink_unicast():
 

int netlink_unicast(struct sock *ssk, struct sk_buff *skb,u32 portid, int nonblock)
{struct sock *sk;int err;long timeo;skb = netlink_trim(skb, gfp_any());timeo = sock_sndtimeo(ssk, nonblock);
retry:sk = netlink_getsockbyportid(ssk, portid);if (IS_ERR(sk)) {kfree_skb(skb);return PTR_ERR(sk);}if (netlink_is_kernel(sk))return netlink_unicast_kernel(sk, skb, ssk);if (sk_filter(sk, skb)) {err = skb->len;kfree_skb(skb);sock_put(sk);return err;}err = netlink_attachskb(sk, skb, &timeo, ssk);if (err == 1)goto retry;if (err)return err;return netlink_sendskb(sk, skb);
}

这里首先调用netlink_trim()重新裁剪skb的数据区的大小,这可能会clone出一个新的skb结构同时重新分配skb->data的内存空间(这就出现了第三次的内存拷贝动作!),当然如果原本skb中多余的内存数据区非常小或者该内存空间是在vmalloc空间中的就不会执行上述操作,我们现在跟随的情景上下文中就是后一种情况,并不会重新分配空间。
接下来记下发送超时等待时间,如果已经设置了MSG_DONTWAIT标识,则等待时间为0,否则返回sk->sk_sndtimeo(该值在sock初始化时由sock_init_data()函数赋值为MAX_SCHEDULE_TIMEOUT)。
接下来根据目的portid号和原端sock结构查找目的端的sock结构:

static struct sock *netlink_getsockbyportid(struct sock *ssk, u32 portid)
{struct sock *sock;struct netlink_sock *nlk;sock = netlink_lookup(sock_net(ssk), ssk->sk_protocol, portid);if (!sock)return ERR_PTR(-ECONNREFUSED);/* Don't bother queuing skb if kernel socket has no input function */nlk = nlk_sk(sock);if (sock->sk_state == NETLINK_CONNECTED &&nlk->dst_portid != nlk_sk(ssk)->portid) {sock_put(sock);return ERR_PTR(-ECONNREFUSED);}return sock;
}

这里首先调用netlink_lookup执行查找工作,查找的命名空间和协议号同原端sock,它会从nl_table[protocol]的哈希表中找到已经注册的目的端sock套接字。找到以后执行校验,如若找到的socket已经connect了,则它的目的portid必须是原端的portid。
接下来判断目的的netlink socket是否是内核的netlink socket:

static inline int netlink_is_kernel(struct sock *sk)
{return nlk_sk(sk)->flags & NETLINK_KERNEL_SOCKET;
}

如果目的地址是内核空间,则调用netlink_unicast_kernel向内核进行单播,入参是目的sock、原端sock和数据skb。否则继续向下执行,现在的情景中,我们跟随用户空间中发送的数据,进入netlink_unicast_kernel()中:

static int netlink_unicast_kernel(struct sock *sk, struct sk_buff *skb,struct sock *ssk)
{int ret;struct netlink_sock *nlk = nlk_sk(sk);ret = -ECONNREFUSED;if (nlk->netlink_rcv != NULL) {ret = skb->len;netlink_skb_set_owner_r(skb, sk);NETLINK_CB(skb).sk = ssk;netlink_deliver_tap_kernel(sk, ssk, skb);nlk->netlink_rcv(skb);consume_skb(skb);} else {kfree_skb(skb);}sock_put(sk);return ret;
}

检查目标netlink套接字是否注册了netlink_rcv()接收函数,如果没有则直接丢弃该数据包,否则继续发送流程,这里首先设置一些标识:
skb->sk = sk;     /* 将目的sock赋值给skb->sk指针 */
skb->destructor = netlink_skb_destructor;   /* 注册destructor钩子函数 */
NETLINK_CB(skb).sk = ssk;   /* 将原端的sock保存早skb的cb扩展字段中 */
最后就调用了nlk->netlink_rcv(skb)函数将消息送到内核中的目的netlink套接字中了。在前一篇文章中已经看到在内核注册netlink套接字的时候已经将其接收函数注册到了netlink_rcv中:

struct sock *
__netlink_kernel_create(struct net *net, int unit, struct module *module,struct netlink_kernel_cfg *cfg)
{......if (cfg && cfg->input)nlk_sk(sk)->netlink_rcv = cfg->input;

对于NETLINK_ROUTE类型的套接字来说就是rtnetlink_rcv了,netlink_rcv()钩子函数会接收并解析用户传下来的数据,不同类型的netlink协议各不相同,这里就不进行分析了。至此应用层下发单播的netlink数据就下发完成了。
现在我们回到netlink_sendmsg()函数中简单分析一下发送组播数据的流程是怎样的:
 

static int netlink_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{......if (dst_group) {atomic_inc(&skb->users);netlink_broadcast(sk, skb, dst_portid, dst_group, GFP_KERNEL);}int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 portid,u32 group, gfp_t allocation)
{return netlink_broadcast_filtered(ssk, skb, portid, group, allocation,NULL, NULL);
}
EXPORT_SYMBOL(netlink_broadcast);int netlink_broadcast_filtered(struct sock *ssk, struct sk_buff *skb, u32 portid,u32 group, gfp_t allocation,int (*filter)(struct sock *dsk, struct sk_buff *skb, void *data),void *filter_data)
{struct net *net = sock_net(ssk);struct netlink_broadcast_data info;struct sock *sk;skb = netlink_trim(skb, allocation);info.exclude_sk = ssk;info.net = net;info.portid = portid;info.group = group;info.failure = 0;info.delivery_failure = 0;info.congested = 0;info.delivered = 0;info.allocation = allocation;info.skb = skb;info.skb2 = NULL;info.tx_filter = filter;info.tx_data = filter_data;/* While we sleep in clone, do not allow to change socket list */netlink_lock_table();sk_for_each_bound(sk, &nl_table[ssk->sk_protocol].mc_list)do_one_broadcast(sk, &info);......
}

这里首先初始化netlink组播数据结构netlink_broadcast_data,其中info.group中保存了目的组播地址,然后从nl_table[ssk->sk_protocol].mc_list里边查找加入组播组的socket,并调用do_one_broadcast()函数依次发送组播数据:

static void do_one_broadcast(struct sock *sk,struct netlink_broadcast_data *p)
{struct netlink_sock *nlk = nlk_sk(sk);int val;if (p->exclude_sk == sk)return;if (nlk->portid == p->portid || p->group - 1 >= nlk->ngroups ||!test_bit(p->group - 1, nlk->groups))return;if (!net_eq(sock_net(sk), p->net))return;if (p->failure) {netlink_overrun(sk);return;}......} else if ((val = netlink_broadcast_deliver(sk, p->skb2)) < 0) {netlink_overrun(sk);if (nlk->flags & NETLINK_BROADCAST_SEND_ERROR)p->delivery_failure = 1;......
}

当然,在发送之前会做一些必要的检查,例如这里会确保原端sock和目的端sock不是同一个,它们属于同一个网络命名空间,目的的组播地址为发送的目的组播地址等等,然后会对skb和组播数据结构netlink_broadcast_data进行一些处理,最后调用netlink_broadcast_deliver()函数对目的sock发送数据skb:

static int netlink_broadcast_deliver(struct sock *sk, struct sk_buff *skb)
{......__netlink_sendskb(sk, skb);......
}static int __netlink_sendskb(struct sock *sk, struct sk_buff *skb)
{int len = skb->len;......skb_queue_tail(&sk->sk_receive_queue, skb);sk->sk_data_ready(sk);return len;
}

可以看到,这里将要发送的skb添加到目的sock的接收队列末尾,然后调用sk_data_ready()通知钩子函数,告知目的sock有数据到达,执行处理流程。对于内核的netlink来说内核netlink的创建函数中已经将其注册为

struct sock *
__netlink_kernel_create(struct net *net, int unit, struct module *module,struct netlink_kernel_cfg *cfg)
{......sk->sk_data_ready = netlink_data_ready;......
}static void netlink_data_ready(struct sock *sk)
{BUG();
}

非常明显了,内核netlink套接字是无论如何也不应该接收到组播消息的。但是对于应用层netlink套接字,该sk_data_ready()钩子函数在初始化netlink函数sock_init_data()中被注册为sock_def_readable(),这个函数后面再分析。至此应用层下发的netlink数据流程就分析结束了。

3 内核向应用层发送netlink消息

                                           

                                        图2 内核发送netlink单播消息

内核可以通过nlmsg_unicast()函数向应用层发送单播消息,由各个netlink协议负责调用,也有的协议是直接调用netlink_unicast()函数,其实nlmsg_unicast()也仅是netlink_unicast()的一个封装而已:

/*** nlmsg_unicast - unicast a netlink message* @sk: netlink socket to spread message to* @skb: netlink message as socket buffer* @portid: netlink portid of the destination socket*/
static inline int nlmsg_unicast(struct sock *sk, struct sk_buff *skb, u32 portid)
{int err;err = netlink_unicast(sk, skb, portid, MSG_DONTWAIT);if (err > 0)err = 0;return err;
}

这里以非阻塞(MSG_DONTWAIT)的形式向应用层发送消息,这时的portid为应用层套接字所绑定的id号。我们再次进入到netlink_unicast()内部,这次由于目的sock不再是内核,所以要走不同的的分支了

int netlink_unicast(struct sock *ssk, struct sk_buff *skb,u32 portid, int nonblock)
{struct sock *sk;int err;long timeo;skb = netlink_trim(skb, gfp_any());timeo = sock_sndtimeo(ssk, nonblock);
retry:sk = netlink_getsockbyportid(ssk, portid);if (IS_ERR(sk)) {kfree_skb(skb);return PTR_ERR(sk);}if (netlink_is_kernel(sk))return netlink_unicast_kernel(sk, skb, ssk);if (sk_filter(sk, skb)) {err = skb->len;kfree_skb(skb);sock_put(sk);return err;}err = netlink_attachskb(sk, skb, &timeo, ssk);if (err == 1)goto retry;if (err)return err;return netlink_sendskb(sk, skb);
}
EXPORT_SYMBOL(netlink_unicast);

这里首先sk_filter执行防火墙的过滤,确保可以发送以后调用netlink_attachskb将要发送的skb绑定到netlink sock上。

int netlink_attachskb(struct sock *sk, struct sk_buff *skb,long *timeo, struct sock *ssk)
{struct netlink_sock *nlk;nlk = nlk_sk(sk);if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||test_bit(NETLINK_CONGESTED, &nlk->state)) &&!netlink_skb_is_mmaped(skb)) {DECLARE_WAITQUEUE(wait, current);if (!*timeo) {if (!ssk || netlink_is_kernel(ssk))netlink_overrun(sk);sock_put(sk);kfree_skb(skb);return -EAGAIN;}__set_current_state(TASK_INTERRUPTIBLE);add_wait_queue(&nlk->wait, &wait);if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||test_bit(NETLINK_CONGESTED, &nlk->state)) &&!sock_flag(sk, SOCK_DEAD))*timeo = schedule_timeout(*timeo);__set_current_state(TASK_RUNNING);remove_wait_queue(&nlk->wait, &wait);sock_put(sk);if (signal_pending(current)) {kfree_skb(skb);return sock_intr_errno(*timeo);}return 1;}netlink_skb_set_owner_r(skb, sk);return 0;
}

如果目的sock的接收缓冲区剩余的的缓存大小小于已经提交的数据量,或者标志位已经置位了阻塞标识NETLINK_CONGESTED,这表明数据不可以立即的送到目的端的接收缓存中。因此,在原端不是内核socket且没有设置非阻塞标识的情况下会定义一个等待队列并等待指定的时间并返回1,否则直接丢弃该skb数据包并返回失败。
若目的端的接收缓存区空间足够,就会调用netlink_skb_set_owner_r进行绑定。
回到netlink_unicast()函数中,可以看到若执行netlink_attachskb()的返回值为1,就会再次尝试发送操作。最后调用netlink_sendskb()执行发送操作:

int netlink_sendskb(struct sock *sk, struct sk_buff *skb)
{int len = __netlink_sendskb(sk, skb);sock_put(sk);return len;
}

这里又一次回到了__netlink_sendskb函数执行发送流程:

static int __netlink_sendskb(struct sock *sk, struct sk_buff *skb)
{int len = skb->len;netlink_deliver_tap(skb);#ifdef CONFIG_NETLINK_MMAPif (netlink_skb_is_mmaped(skb))netlink_queue_mmaped_skb(sk, skb);else if (netlink_rx_is_mmaped(sk))netlink_ring_set_copied(sk, skb);else
#endif /* CONFIG_NETLINK_MMAP */skb_queue_tail(&sk->sk_receive_queue, skb);sk->sk_data_ready(sk);return len;
}

这里的sk_data_ready()钩子函数在初始化netlink函数sock_init_data()中被注册为sock_def_readable(),进入分析一下:

static void sock_def_readable(struct sock *sk)
{struct socket_wq *wq;rcu_read_lock();wq = rcu_dereference(sk->sk_wq);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();
}

这里唤醒目的接收端socket的等待队列,这样应用层套接字就可以接收并处理消息了。
以上分析了内核发送单播消息,下面来看一下内核发送多播消息流程,该流程非常简单,它的入口是 nlmsg_multicast():

static inline int nlmsg_multicast(struct sock *sk, struct sk_buff *skb,u32 portid, unsigned int group, gfp_t flags)
{int err;NETLINK_CB(skb).dst_group = group;err = netlink_broadcast(sk, skb, portid, group, flags);if (err > 0)err = 0;return err;
}

nlmsg_multicast及后续的流程前文中都已分析过了,此处不再赘述。至此内核发送netlink消息已经完成,下满来看一下应用层是如何接收该消息的。

4 应用层接收内核netlink消息

使用如下示例程序可以以阻塞的方式接收内核发送的netlink消息:

#define TEST_DATA_LEN	16struct sockaddr_nl nladdr;
struct msghdr msg;
struct nlmsghdr *nlhdr;
struct iovec iov;/* 清空源地址结构 */
memset(&nladdr, 0, sizeof(nladdr));/* 清空netlink消息头 */
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(TEST_DATA_LEN));
memset(nlhdr, 0, NLMSG_SPACE(TEST_DATA_LEN));/* 封装netlink消息 */
iov.iov_base = (void *)nlhdr;					/* 接收缓存地址 */
iov.iov_len = NLMSG_LENGTH(TEST_DATA_LEN);;		/* 接收缓存大小 *//* 填充数据消息结构 */
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);				/* 地址长度由内核赋值 */
msg.msg_iov = &iov;
msg.msg_iovlen = 1;/* 接收netlink消息 */
recvmsg(sock_fd, &msg, 0);

本示例程序同前文中的发送程序类似,需要有接收端组装接收msg消息。同发送流程的不同之处在于:
(1)msg.msg_name地址结构中存放的是消息源的地址信息,由内核负责填充。
(2)iov.iov_base为接收缓存的地址空间,其需要在接收前清空。
(3)iov.iov_len为单个iov接收缓存的长度,需要指明。
(4)msg.msg_namelen:为地址占用长度,有内核负责填充。
(5)msg.msg_iovlen:为接收iov空间的个数,需要指明。
这里用到了recvmsg系统调用,现进入该系统调用分析消息的整个接收的过程(需要注意的是,在不使用NETLINK_MMAP技术的情况下,整个接收的过程中存在1次数据的内存拷贝动作!):

       

                 图3 用户态netlink接收流程

SYSCALL_DEFINE3(recvmsg, int, fd, struct user_msghdr __user *, msg,unsigned int, flags)
{if (flags & MSG_CMSG_COMPAT)return -EINVAL;return __sys_recvmsg(fd, msg, flags);
}long __sys_recvmsg(int fd, struct user_msghdr __user *msg, unsigned flags)
{int fput_needed, err;struct msghdr msg_sys;struct socket *sock;sock = sockfd_lookup_light(fd, &err, &fput_needed);if (!sock)goto out;err = ___sys_recvmsg(sock, msg, &msg_sys, flags, 0);fput_light(sock->file, fput_needed);
out:return err;
}

同sendmsg系统调用类似,这里也同样首先通过fd描述符查找对应的套接字socket结构,然后调用___sys_recvmsg()执行实际的工作,这个函数比较长,分段分析:

static int ___sys_recvmsg(struct socket *sock, struct user_msghdr __user *msg,struct msghdr *msg_sys, unsigned int flags, int nosec)
{struct compat_msghdr __user *msg_compat =(struct compat_msghdr __user *)msg;struct iovec iovstack[UIO_FASTIOV];struct iovec *iov = iovstack;unsigned long cmsg_ptr;int total_len, len;ssize_t err;/* kernel mode address */struct sockaddr_storage addr;/* user mode address pointers */struct sockaddr __user *uaddr;int __user *uaddr_len = COMPAT_NAMELEN(msg);msg_sys->msg_name = &addr;

同sendmsg类似,这里同样定义了一个大小为8的iovstack数组缓存,用来加速消息处理;随后获取用户空间的地址长度字段的地址。

	if (MSG_CMSG_COMPAT & flags)err = get_compat_msghdr(msg_sys, msg_compat, &uaddr, &iov);elseerr = copy_msghdr_from_user(msg_sys, msg, &uaddr, &iov);if (err < 0)return err;total_len = iov_iter_count(&msg_sys->msg_iter);cmsg_ptr = (unsigned long)msg_sys->msg_control;msg_sys->msg_flags = flags & (MSG_CMSG_CLOEXEC|MSG_CMSG_COMPAT);

这里接着调用copy_msghdr_from_user拷贝用户态msg中的数据到内核态msg_sys中。当然这里主要是为了接收内核的消息,用户空间并没有什么实际的数据,这里最主要的作用就是确定用户需要接收多少数据量。注意第三个参数已经不再是NULL了,而是指向了uaddr指针的地址,再次进入该函数详细分析一下:

static int copy_msghdr_from_user(struct msghdr *kmsg,struct user_msghdr __user *umsg,struct sockaddr __user **save_addr,struct iovec **iov)
{struct sockaddr __user *uaddr;struct iovec __user *uiov;size_t nr_segs;ssize_t err;if (!access_ok(VERIFY_READ, umsg, sizeof(*umsg)) ||
<span style="color:#ff0000;">	    __get_user(uaddr, &umsg->msg_name) ||</span>__get_user(kmsg->msg_namelen, &umsg->msg_namelen) ||__get_user(uiov, &umsg->msg_iov) ||__get_user(nr_segs, &umsg->msg_iovlen) ||__get_user(kmsg->msg_control, &umsg->msg_control) ||__get_user(kmsg->msg_controllen, &umsg->msg_controllen) ||__get_user(kmsg->msg_flags, &umsg->msg_flags))return -EFAULT;if (!uaddr)kmsg->msg_namelen = 0;if (kmsg->msg_namelen < 0)return -EINVAL;if (kmsg->msg_namelen > sizeof(struct sockaddr_storage))kmsg->msg_namelen = sizeof(struct sockaddr_storage);<span style="color:#ff0000;">	if (save_addr)*save_addr = uaddr;</span>if (uaddr && kmsg->msg_namelen) {
<span style="color:#ff0000;">		if (!save_addr) {err = move_addr_to_kernel(uaddr, kmsg->msg_namelen,kmsg->msg_name);</span>if (err < 0)return err;}} else {kmsg->msg_name = NULL;kmsg->msg_namelen = 0;}if (nr_segs > UIO_MAXIOV)return -EMSGSIZE;kmsg->msg_iocb = NULL;return import_iovec(<span style="color:#ff0000;">save_addr ? READ : WRITE</span>, uiov, nr_segs,UIO_FASTIOV, iov, &kmsg->msg_iter);
}

注意到其中加红的这几行,其中传入的uaddr指针被指向了用户空间msg->msg_name地址处,然后内核也不再会调用move_addr_to_kernel将用户空间的消息地址字段拷贝到内核空间了(因为根本没必要了),然后以READ的方式调用import_iovec()函数,它会检查用户空间的消息数据地址是否可以写入,然后根据用户需要接收的msg_iovlen长度封装kmsg->msg_iter结构。再回到___sys_recvmsg()函数中保存接收缓存的总长度到total_len中,然后设置flag标识。

	/* We assume all kernel code knows the size of sockaddr_storage */msg_sys->msg_namelen = 0;if (sock->file->f_flags & O_NONBLOCK)flags |= MSG_DONTWAIT;err = (nosec ? sock_recvmsg_nosec : sock_recvmsg)(sock, msg_sys,total_len, flags);

这里将地址的长度字段清零,不使用用户空间传入的(这里假定内核知道地址的长度),然后调用根据nosec的值是否为0而调用sock_recvmsg_nosec()或sock_recvmsg()函数接收数据,nosec在recvmsg系统调用传入的为0,在recvmmsg系统能够调用接收多个消息时传入已经接受的消息个数。
同发送的sendmsg()和sendmmsg()两个系统调用一样,这样设计也是为了加速消息接收,recvmmsg()就是sock_recvmsg_nosec()的一个封装而已,只不过会增加security检查:

int sock_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,int flags)
{int err = security_socket_recvmsg(sock, msg, size, flags);return err ?: sock_recvmsg_nosec(sock, msg, size, flags);
}
EXPORT_SYMBOL(sock_recvmsg);static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,size_t size, int flags)
{return sock->ops->recvmsg(sock, msg, size, flags);
}

这里调用了接收套接字所在协议的recvmsg接收钩子函数,对于netlink就是netlink_recvmsg()函数:

static int netlink_recvmsg(struct socket *sock, struct msghdr *msg, size_t len,int flags)
{struct scm_cookie scm;struct sock *sk = sock->sk;struct netlink_sock *nlk = nlk_sk(sk);int noblock = flags&MSG_DONTWAIT;size_t copied;struct sk_buff *skb, *data_skb;int err, ret;if (flags&MSG_OOB)return -EOPNOTSUPP;copied = 0;skb = skb_recv_datagram(sk, flags, noblock, &err);if (skb == NULL)goto out;data_skb = skb;

这里首先调用skb_recv_datagram()从接收socket的缓存中接收消息并通过skb返回,如果设置了MSG_DONTWAIT则在接收队列中没有消息时立即返回,否则会阻塞等待。进入该函数详细分析:

struct sk_buff *skb_recv_datagram(struct sock *sk, unsigned int flags,int noblock, int *err)
{int peeked, off = 0;return __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0),&peeked, &off, err);
}
EXPORT_SYMBOL(skb_recv_datagram);struct sk_buff *__skb_recv_datagram(struct sock *sk, unsigned int flags,int *peeked, int *off, int *err)
{struct sk_buff_head *queue = &sk->sk_receive_queue;struct sk_buff *skb, *last;unsigned long cpu_flags;long timeo;/** Caller is allowed not to check sk->sk_err before skb_recv_datagram()*/int error = sock_error(sk);if (error)goto no_packet;timeo = sock_rcvtimeo(sk, flags & MSG_DONTWAIT);do {/* Again only user level code calls this function, so nothing* interrupt level will suddenly eat the receive_queue.** Look at current nfs client by the way...* However, this function was correct in any case. 8)*/int _off = *off;last = (struct sk_buff *)queue;spin_lock_irqsave(&queue->lock, cpu_flags);skb_queue_walk(queue, skb) {last = skb;*peeked = skb->peeked;if (flags & MSG_PEEK) {if (_off >= skb->len && (skb->len || _off ||skb->peeked)) {_off -= skb->len;continue;}skb = skb_set_peeked(skb);error = PTR_ERR(skb);if (IS_ERR(skb))goto unlock_err;atomic_inc(&skb->users);} else__skb_unlink(skb, queue);spin_unlock_irqrestore(&queue->lock, cpu_flags);*off = _off;return skb;}spin_unlock_irqrestore(&queue->lock, cpu_flags);if (sk_can_busy_loop(sk) &&sk_busy_loop(sk, flags & MSG_DONTWAIT))continue;/* User doesn't want to wait */error = -EAGAIN;if (!timeo)goto no_packet;} while (!wait_for_more_packets(sk, err, &timeo, last));return NULL;unlock_err:spin_unlock_irqrestore(&queue->lock, cpu_flags);
no_packet:*err = error;return NULL;
}

首先获取socket的接收队列指针到保存到queue变量中,然后获取等待时长sk->sk_rcvtimeo(它在socket初始化时被设置为MAX_SCHEDULE_TIMEOUT,也可通过set_socketopt修改),接下来进入一个do while()循环等待从接收缓存中获取数据。
首先假定当前接收队列中已经有数据了,这时将队列上锁后从队列中取出一个skb包,然后判断是否设置了MSG_PEEK标识符(如果已经设置了表明仅获取该skb包但是不从接收队列中删除),若设置了则调用skb_set_peeked()函数skb_clone出一个skb消息包返回,否则直接调用__skb_unlink将本次取出的skb包从列表中删除然后返回。
然后再假定当前接收队列中没有消息存在,这里会根据是否设置了CONFIG_NET_RX_BUSY_POLL内核选项执行两种等待方案。若设置了改选项,则会尝试使用一种忙等待的方案(类似于设备驱动的napi),不用来让当前执行流睡眠而切出,可以加速数据的接收速度,它会调用sk_can_busy_loop()和sk_busy_loop()判断是否可以使用该方案。对于没有设置该选项,则使用传统的方案,调用wait_for_more_packets()执行等待操作,它加入等待队列会让进程睡眠:

static int wait_for_more_packets(struct sock *sk, int *err, long *timeo_p,const struct sk_buff *skb)
{int error;DEFINE_WAIT_FUNC(wait, receiver_wake_function);prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);....../* handle signals */if (signal_pending(current))goto interrupted;error = 0;*timeo_p = schedule_timeout(*timeo_p);
out:finish_wait(sk_sleep(sk), &wait);return error;
interrupted:error = sock_intr_errno(*timeo_p);	......
}

该函数会做一些必要的判断,但是最核心的就是以上这几行代码。首先将进程设置为INTERRUPTIBLE状态,若没有消息到达或者接收到信号它会一直睡眠(超时时间timeo可设,一般为默认值MAX_SCHEDULE_TIMEOU的情况下会转而调用schedule())在schedule_timeout()函数上。在前文中已经看到,在内核netlink发送流程的最后将数据送到接收队列后会唤醒该等待队列,进程唤醒后返回到__skb_recv_datagram的大循环中去接收消息,若是被信号打断了,那大循环中无法获取消息,又会回到这里执行信号退出动作。
在从接收队列中获取了一个skb以后,我们回到netlink_recvmsg()函数中继续往下分析:

	/* Record the max length of recvmsg() calls for future allocations */nlk->max_recvmsg_len = max(nlk->max_recvmsg_len, len);nlk->max_recvmsg_len = min_t(size_t, nlk->max_recvmsg_len,16384);copied = data_skb->len;if (len < copied) {msg->msg_flags |= MSG_TRUNC;copied = len;}skb_reset_transport_header(data_skb);err = skb_copy_datagram_msg(data_skb, 0, msg, copied);

这里更新了最长的的接收数据长度,然后判断如果获取到的skb数据长度大于大于本次接收缓存的最大长度,则设置MSG_TRUNC标识,并将本次需要接收数据量设置为接收缓存的长度。
接着调用skb_copy_datagram_msg()函数将skb中的实际数据拷贝到msg消息中(这里进行了一次数据拷贝动作,将skb中的数据直接拷贝到msg指向的用户空间地址处)。

	if (msg->msg_name) {DECLARE_SOCKADDR(struct sockaddr_nl *, addr, msg->msg_name);addr->nl_family = AF_NETLINK;addr->nl_pad    = 0;addr->nl_pid	= NETLINK_CB(skb).portid;addr->nl_groups	= netlink_group_mask(NETLINK_CB(skb).dst_group);msg->msg_namelen = sizeof(*addr);}

在拷贝完成后这里开始初始化地址结构,这里将family这是为AF_NETLINK地址族,然后设置portid号为保存在原端skb扩展cb字段中的portid,对于这里接收内核发送的skb消息来说本字段为0,然后设置组播地址,该值在前文中内核调用nlmsg_multicast()发送组播消息时设置(对于单播来说就为0),netlink_group_mask()函数将组播地址的位号转换为实际的组播地址(mask),然后这是msg的地址长度为nl_addr的长度。

	if (nlk->flags & NETLINK_RECV_PKTINFO)netlink_cmsg_recv_pktinfo(msg, skb);memset(&scm, 0, sizeof(scm));scm.creds = *NETLINK_CREDS(skb);if (flags & MSG_TRUNC)copied = data_skb->len;skb_free_datagram(sk, skb);if (nlk->cb_running &&atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf / 2) {ret = netlink_dump(sk);if (ret) {sk->sk_err = -ret;sk->sk_error_report(sk);}}scm_recv(sock, msg, &scm, flags);
out:netlink_rcv_wake(sk);return err ? : copied;
}

这里如果设置了NETLINK_RECV_PKTINFO标识则将辅助消息头拷贝到用户空间。接着判断是否设置了MSG_TRUNC标识,如果设置了就重新设置copied为本次取出的skb中获取数据的长度(特别注意!)。然后调用skb_free_datagram()释放skb消息包,最后在返回接收数据长度。再回到___sys_recvmsg()函数中继续往下分析:

	err = (nosec ? sock_recvmsg_nosec : sock_recvmsg)(sock, msg_sys,total_len, flags);if (err < 0)goto out_freeiov;len = err;if (uaddr != NULL) {err = move_addr_to_user(&addr,msg_sys->msg_namelen, uaddr,uaddr_len);if (err < 0)goto out_freeiov;}

这里len保存了接收到数据的长度,然后将消息地址信息从内核空间拷贝到用户空间。

	err = __put_user((msg_sys->msg_flags & ~MSG_CMSG_COMPAT),COMPAT_FLAGS(msg));if (err)goto out_freeiov;if (MSG_CMSG_COMPAT & flags)err = __put_user((unsigned long)msg_sys->msg_control - cmsg_ptr,&msg_compat->msg_controllen);elseerr = __put_user((unsigned long)msg_sys->msg_control - cmsg_ptr,&msg->msg_controllen);if (err)goto out_freeiov;err = len;out_freeiov:kfree(iov);return err;
}

最后将flag、消息辅助数据等复制到用户空间,至此recvmsg系统调用就向上返回了,应用层也可以使用获取到的数据了。应用层接收netlink消息流程结束。
 

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. [redis] 10 种数据结构详解

    [redis] 10 种数据结构详解 简介 5种常见数据结构 string: 最常见的 string key valuelist: 双向链表set: 集合-zset: 有序集合hash: 类似 Java HashMap 和 golang 的 map 有 2 级索引 原语------介绍常见场景stringstring key/value 最常见的数据类型最常用缓存; 分布式锁…...

    2024/5/8 3:29:16
  2. Python 简介

    Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 是一种解释型语言&#xff1a; 这意味着开发过程中没有了编译这个环节。类似于PHP和Perl语言。 Python 是交互式语言&#xff1a; 这意味着&#xff0c;您可以在一个 Python 提示符 >>…...

    2024/5/8 2:24:02
  3. Unity之引导功能遮罩事件穿透

    Unity之新手引导shader遮罩事件穿透 效果图 设计思路 1.新手引导我们期待开发内容不影响正常的功能模块&#xff0c;意思就是分层&#xff0c;新手引导在正常功能之上 2.新手引导层级用一层深色bg显示遮住正常功能层级&#xff0c;在需要引导的位置留出高亮区域 3.在这个新手…...

    2024/4/20 5:57:49
  4. 主机加固概念介绍

    最近公司做服务器安全&#xff0c;开始在市场了解产品&#xff0c;对这一块算是短暂的研究了一段时间&#xff0c;有一点心得给大家分享一下。 主机加固 最近主机加固的概念被炒得火热&#xff0c;主机加固的功能也正在被致力于服务器安全的相关人士所关注。 那么究竟什么是主…...

    2024/5/8 1:01:08
  5. 风尚云网-JS相关综合面试题(不看后悔系列)持续更新...

    目录 简述一下Promise原理&#xff1f; 简述一下继承与原型链&#xff1f; 判断 js 类型的几种方式&#xff1f; 闭包的概念&#xff1f;优缺点&#xff1f; 数组去重的方法&#xff1f; DOM 事件有哪些阶段&#xff1f;谈谈对事件代理的理解&#xff1f; ES6 的 class …...

    2024/4/13 8:18:00
  6. 深入理解Java虚拟机——Java内存区域

    目录一、Java内存区域——简介二、Java内存区域——程序计数器三、Java内存区域——java虚拟机栈3.1、虚拟机栈3.2、栈帧3.3、局部变量表3.4、虚拟机栈大小四、Java内存区域——本地方法栈五、Java内存区域——java堆六、Java内存区域——方法区七、Java内存区域——直接内存和…...

    2024/4/13 8:18:00
  7. 蓝桥杯嵌入式STM32G431——第六届省赛真题

    第六届省赛真题 main.c #include "main.h" //使用CubeMX配置以下头文件下的模块初始化 #include "rcc.h" //时钟初始化 #include "led_key.h" #include "lcd.h" #include "i2c.h" #include "uart.h" #include &q…...

    2024/5/7 15:59:16
  8. Reddit进行测试,允许用户像Twitter那样将任何NFT设为个人资料图片

    Reddit正在测试一项功能&#xff0c;允许用户将自己拥有的任何NFT设为个人资料图片——而不仅仅是Reddit自己基于以太坊的NFT&#xff0c;也就是去年限量发行的“CryptoSnoo”。Twitter在最近推出了一个类似的功能&#xff0c;允许用户将自己的NFT设为个人资料图片&#xff0c;…...

    2024/5/7 17:59:22
  9. Iterator和for...of..循环

    一、Iterator遍历器 1、Iterator&#xff08;遍历器&#xff09;的概念 遍历器Iterator是一种接口&#xff0c;为各种不同的数据结构提供统一的访问机制。任何数据结构&#xff0c;只要部署Iterator接口&#xff0c;就可以完成遍历操作。 Iterator的作用有3个&#xff1a; …...

    2024/5/7 16:47:08
  10. pair排序

    bool cmp(pll &a,pll &b){return a.first*b.second<b.first*a.second;}...

    2024/4/26 9:07:18
  11. LINUX命令行设置代理上网

    /etc/resolv.conf修改DNS /etc/profile增加 export http_proxy=http://domain\\user:pwd@proxy_addr:port export https_proxy= http://domain\\user:pwd@proxy_addr:port export ftp_proxy= http://domain\\user:pwd@proxy_addr:port export no_proxy=proxy_addr:…...

    2024/5/7 22:36:25
  12. 类、对象和构造器的介绍

    面向对象 ①面向过程:针对于我该如何去做 ②面向对象:针对于我该让谁来做 类:将代码的相同状态和行为抽出,然后放入到一个模块中&#xff0c;这就是类 相同的状态:属性 相同的行为:方法 声明: 权限修饰符 class 类名 {}对象:是类的具体实例的描述 声明: 类名 对象名 new …...

    2024/5/8 5:52:27
  13. js基础2

    #1 基础语法 1.1数组 数组是按次序排列的一组数据&#xff0c;每个值的位置都有编号&#xff08;从0开始&#xff09;&#xff0c;整个数组用方括号表示 1.1.1数组定义 ​ js中定义数组的三种方式 var arr[值1,值2];//隐式创建var arrnew Array(值1,值2);//直接实例化var …...

    2024/5/7 18:29:05
  14. shell脚本之基础入门

    Shell 教程 Shell 是一个用 C 语言编写的程序&#xff0c;它是用户使用 Linux 的桥梁。Shell 既是一种命令语言&#xff0c;又是一种程序设计语言。 Shell 是指一种应用程序&#xff0c;这个应用程序提供了一个界面&#xff0c;用户通过这个界面访问操作系统内核的服务。 第一个…...

    2024/4/13 8:19:10
  15. 【python教程入门学习】通过运行python脚本来更改Windows背景

    通过运行python脚本来更改Windows背景 在我们开始之前&#xff0c;一定要注意这篇文章只针对Windows用户&#xff01;对于那些使用Windows的人来说&#xff0c;这是一个有趣的想法。 如果您想使用python更改桌面背景&#xff0c;您会做什么&#xff1f;您可能要去一个库或寻找…...

    2024/4/19 14:28:19
  16. 语音识别损失函数chain 和ctc 的异同

    总结&#xff1a; 1个相同点&#xff0c; 4个不同点。 相同点&#xff1a; 都是在label生成的多条特定路径后的前向后向运算 不同点&#xff1a; label生成的多条特定路径的方式不同。 chain numerator的多条特定路径是alignment出来的&#xff1b; ctc是通过下图方式&…...

    2024/4/13 8:18:55
  17. 101. Symmetric Tree. Sol

    提供一种很有意思的递归解法 Given the root of a binary tree, check whether it is a mirror of itself(i.e., symmetric around its center). Example 1: Input: root [1,2,2,3,4,4,3] Output: trueExample 2: Input: root [1,2,2,null,3,null,3] Output: falseConstrain…...

    2024/4/19 14:19:03
  18. Resolved [java.lang.NullPointerException]

    2022-02-10 16:48:16.014 ERROR 6616 — [nio-8080-exec-4] com.own.blog.handler.ControllerHandler : requestUrl : http://localhost:8080/admin/blogs , Exception : null 2022-02-10 16:48:16.015 WARN 6616 — [nio-8080-exec-4] .m.m.a.ExceptionHandlerExceptionResolv…...

    2024/4/28 23:47:40
  19. 自主式水下机器人的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告

    本文研究全球与中国市场自主式水下机器人的发展现状及未来发展趋势&#xff0c;分别从生产和消费的角度分析自主式水下机器人的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国…...

    2024/4/13 8:18:55
  20. 【Data Governance】数据治理的6个挑战

    企业越大&#xff0c;需要的数据和产生的数据也就越多&#xff0c;而数据越多则意味着就越需要定制适合企业自身的正式且有效的数据质量策略。在向着数字化快速迈进的同时&#xff0c;当前企业数据治理面临着各种挑战&#xff0c;主要表现为以下6个方面。 对数据治理的业务价值…...

    2024/4/19 14:46:18

最新文章

  1. 【unity】(2)GameObject

    GameObject类是基本的游戏对象类型&#xff0c;它可以用来代表场景中的任何实体。 基本属性 name 类型&#xff1a;string说明&#xff1a;GameObject的名称。用法&#xff1a; GameObject go new GameObject(); go.name "My GameObject";activeSelf 类型&#xf…...

    2024/5/8 7:08:58
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/5/7 10:36:02
  3. 【讲解下Docker in Docker的原理与实践】

    &#x1f308;个人主页:程序员不想敲代码啊&#x1f308; &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家&#x1f3c6; &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提…...

    2024/5/7 21:23:01
  4. 程序员开发技术整理(持续整理中)

    前端技术&#xff1a; vue-前端框架element-前端框架bootstrap-前端框架echarts-图标组件 C#后端技术&#xff1a; webservice&#xff1a;soap架构&#xff1a;简单的通信协议&#xff0c;用于服务通信ORM框架&#xff1a;对象关系映射&#xff0c;如EF&#xff1a;对象实体…...

    2024/5/7 15:55:32
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/8 6:01:22
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/7 9:45:25
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/5/4 23:54:56
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/7 14:25:14
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/5/4 23:54:56
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/5/4 23:55:05
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/5/4 23:54:56
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/5/7 11:36:39
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/5/4 23:54:56
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/6 1:40:42
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/5/4 23:54:56
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/5/4 23:55:17
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/5/7 9:26:26
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/5/4 23:54:56
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/4 23:55:06
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/5/5 8:13:33
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/5/4 23:55:16
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/5/4 23:54:58
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/6 21:42:42
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/5/4 23:54:56
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57