一、什么是netlink

Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。

在Linux 内核中,使用netlink 进行应用与内核通信的应用有很多,如
路由 daemon(NETLINK_ROUTE)
用户态 socket 协议(NETLINK_USERSOCK)
防火墙(NETLINK_FIREWALL)
netfilter 子系统(NETLINK_NETFILTER)
内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)
通用netlink(NETLINK_GENERIC)

Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、Netlink。而前两种都是单向的,而Netlink可以实现双工通信。

Netlink 相对于系统调用,ioctl 以及 /proc文件系统而言,具有以下优点

1、netlink使用简单,只需要在include/linux/netlink.h中增加一个新类型的 netlink 协议定义即可,(如 #define NETLINK_TEST 20 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换)
2、netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息。使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖
3、netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性。内核可以使用 netlink 首先发起会话

Netlink协议基于BSD socket和AF_NETLINK地址簇,使用32位的端口号寻址,每个Netlink协议通常与一个或一组内核服务/组件相关联,如NETLINK_ROUTE用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT用于内核向用户空间的udev进程发送通知等。

netlink架构图

二、用户态下使用netlink

用户态使用标准的socket API如socket,bind,sendmsg,recvmsg和close等接口就能很容易地使用netlink socket。注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。当然 socket 需要的头文件也必不可少,sys/socket.h

1)、创建netlink socket

        sock_fd = socket(AF_NETLINK/PF_NETLINK, int type, int protocol)

其中,type可以取SOCK_RAW或者SOCK_DGRAM。protocol指定netlink协议类型。当前支持的协议类型定义如下:

目前 netlink 协议族支持32种协议类型,内核使用了21种,定义在include/uapi/linux/netlink.h:

#define NETLINK_ROUTE       0   // Routing/device hook ,用于设置和查询路由表等网络核心模块
#define NETLINK_UNUSED      1   /* Unused number                */
#define NETLINK_USERSOCK    2   /* Reserved for user mode socket protocols  */
#define NETLINK_FIREWALL    3   /* Unused number, formerly ip_queue     */
#define NETLINK_SOCK_DIAG   4   /* socket monitoring                */
#define NETLINK_NFLOG       5   /* netfilter/iptables ULOG */
#define NETLINK_XFRM        6   /* ipsec */
#define NETLINK_SELINUX     7   /* SELinux event notifications */
#define NETLINK_ISCSI       8   /* Open-iSCSI */
#define NETLINK_AUDIT       9   /* auditing */
#define NETLINK_FIB_LOOKUP  10
#define NETLINK_CONNECTOR   11
#define NETLINK_NETFILTER   12  /* netfilter subsystem */
#define NETLINK_IP6_FW      13
#define NETLINK_DNRTMSG     14  /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT  15  // Kernel messages to userspace,用于uevent消息通信
#define NETLINK_GENERIC     16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT   18  /* SCSI Transports */
#define NETLINK_ECRYPTFS    19
#define NETLINK_RDMA        20
#define NETLINK_CRYPTO      21  /* Crypto layer */
#define NETLINK_INET_DIAG   NETLINK_SOCK_DIAG
#define MAX_LINKS 32</span>

2)、将netlink socket和进程绑定

  bind(sock_fd, (struct sockaddr*)&nl_addr, sizeof(&nl_addr));

函数bind用于把一个打开的netlink socket与进程进行绑定,需要进行绑定的netlink socket地址结构如下:

//netlink socket地址结构体

struct sockaddr_nl {__kernel_sa_family_t    nl_family; /* 固定取AF_NETLINK或者PF_NETLINK,无差别 */unsigned short    nl_pad;          /* 填充字段,取zero        */__u32          nl_pid;          /* 当前进程port ID,一般通过getpid获取      */__u32          nl_groups; //multicast groups mask ,用于指定多播组,每一个bit对应一个多播组,如果设置为0,表示不加入任何多播组};

3)、发送netlink消息

为了能够把netlink消息发送给内核或者别的用户进程,需要使用另外一个结构体struct sockaddr_nl作为目的地址。如果消息是发往内核的话,nl_pid和nl_groups都应该设置为0,;如果消息是发往另一个进程,nl_pid应该设置为接受者进程的PID,nl_groups用于设置需要发往的多播组。

填充好了目的地址后,就可以将netlink地址应用到结构体struct msghdr中,供函数sendmsg来调用:

使用函数 sendmsg 发送 netlink 消息时还需要引用结构 struct msghdr、struct nlmsghdr 和 struct iovec

①struct msghdr {void         *msg_name;       /* optional address */socklen_t     msg_namelen;    /* size of address */struct iovec *msg_iov;        /* scatter/gather array */size_t        msg_iovlen;     /* # elements in msg_iov */void         *msg_control;    /* ancillary data, see below */size_t        msg_controllen; /* ancillary data buffer len */int           msg_flags;      /* flags on received message */};

结构 struct msghdr 需如下设置:

struct msghdr msg;memset(&msg, 0, sizeof(msg));msg.msg_name = (void *)&nladdr;      /*  nladdr 为消息接收者的 netlink 地址  */msg.msg_namelen = sizeof(nladdr);

由于linux内核的netlink部分总是认为每个netlink消息体中已经包含了下面的消息头,netlink socket 自己的消息头。所以每个应用程序在发送netlink消息之前需要提供这个头信息:

②//netlink message header

struct nlmsghdr {__u32          nlmsg_len; /* Length of message including header 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小*/__u16          nlmsg_type;      /* Message content 用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,因此大部分情况下设置为 0*/__u16          nlmsg_flags;  /* Additional flags */__u32          nlmsg_seq; /* 用于应用追踪消息,表示顺序号 */__u32          nlmsg_pid; /* Sending process port ID, 消息来源进程 ID*/};

标注:字段 nlmsg_flags 用于设置消息标志,可用的标志包括:

/* Flags values */
#define NLM_F_REQUEST           1       /* It is request message.    用于表示消息是一个请求,所有应 用首先发起的消息都应设置该标志。  */
#define NLM_F_MULTI             2       /* Multipart message, terminated by NLMSG_DONE  指示该消息   是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得*/
#define NLM_F_ACK               4       /* Reply with ack, with zero or error code 表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来*/
#define NLM_F_ECHO              8       /* Echo this request    表示该消息是相关的一个包的回传 */
/* Modifiers to GET request */
#define NLM_F_ROOT      0x100   /* specify tree root    */
/* 标志NLM_F_ROOT 被许多netlink协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置 NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型。*/
#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   用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败*/
#define NLM_F_CREATE    0x400   /* Create, if it does not exist指示应当在指定的表中创建一个条目 */
#define NLM_F_APPEND    0x800   /* Add to end of list   在表末尾添加新的条目 */

内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作)
填充完消息头后,在消息头后面就可以填充消息体的内容了,填充完消息体,使用struct iovec结构体,使iov_base指向包含netlink消息的缓冲区,即可调用sendmsg接口发送netlink消息。struct iovec结构定义如下:

③struct iovec
{
void __user *iov_base;    /* BSD uses caddr_t (1003.1g requires void *)    iov_base指向数据包缓冲区,即参数buff*/__kernel_size_t iov_len; /* Must be size_t (1003.1g) */   /* iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff)  */
};

在完成以上步骤后,消息就可以通过下面语句直接发送:发送netlink消息的代码如下:

struct iovec iov;iov.iov_base = (void *)msg_buffer;iov.iov_len = nlh->nlmsg_len;msg.msg_iov = &iov;msg.msg_iovlen = 1;sendmsg(sock_fd, &msg, 0);

4)、接收消息

接收程序需要申请足够大的空间来存储netlink消息头和消息的payload部分。用如下的方式填充结构体struct msghdr,然后调用recvmsg接口来接收netlink消息:

char msg_buffer[MSX_NL_MSG_LEN];struct sockaddr_nl nladdr;struct msghdr msg;struct iovec iov;iov.iov_base = (void *)msg_buffer;iov.iov_len = MAX_NL_MSG_LEN;msg.msg_name = (void *)&(nladdr);msg.msg_namelen = sizeof(nladdr);   msg.msg_iov = &iov;msg.msg_iovlen = 1;recvmsg(sock_fd, &msg, 0);    /*  sock_fd为socket调用打开的netlink socket描述符  */

当消息正确接收后,msg_buffer里保存包含netlink消息头的整个netlink消息,nlhdr指向接收到的消息的消息头,nladdr包含接收到的消息体的目的地址信息。宏NLMSG_DATA(nlhdr)返回指向消息的数据部分的指针(宏部分会介绍到)。

5)、关闭netlink socket

使用完前面创建的netlink socket后,就可以使用close接口关闭netlink socket,释放socket资源。关闭netlink socket的代码与关闭其他socket一致,代码如下: close(sock_fd);

netlink常用宏:在linux/netlink.h中定义了一些方便对消息进行处理的宏

#define NLMSG_ALIGNTO   4

①/* 宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值 */

#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

②/* Netlink 头部长度 */

#define NLMSG_HDRLEN     ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))

③/* 计算消息数据len的真实消息长度(消息体 + 消息头),一般用于分配消息缓存*/

#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)

④/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,也用于分配消息缓存 */

#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))

⑤/* 宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取接收消息数据部分时需要使用该宏 */

#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))

⑥/* 宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址, 同时len 变为剩余消息的长度。该宏一般在一个消息被分成几个部分发送或接收时使用。 */

#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))

⑧/* NLMSG_PAYLOAD(nlh,len) 用于返回payload的长度*/

#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

三、内核态netlink使用

内核空间的netlink API接口是由内核中的netlink核心代码支持的,在net/core/af_netlink.c中实现。内核模块通过这些API访问netlink socket并与用户空间的程序进行通信。

1) 添加自定义协议类型

内核已经支持的协议类型在linux/netlink.h中定义,如果要使用自定义的协议类型,需要在此文件中新增一个协议类型,然后就可以在linux内核模块中使用这个协议类型了。也就是内核模块要想使用netlink,必须包含头文件linux /netlink.h。内核使用netlink需要专门的API,这完全不同于用户态应用对netlink的使用。如果用户需要增加新的netlink协 议类型,必须通过修改linux/netlink.h来实现,当然,目前的netlink实现已经包含了一个通用的协议类型 NETLINK_GENERIC以方便用户使用,用户可以直接使用它而不必增加新的协议类型。

2) 在内核创建netlink socket

在内核空间,通过如下接口创建netlink socket:

struct sock *netlink_kernel_create(struct net *net, int unit, unsigned int groups, void (*input)(struct            sk_buff *skb), struct mutex *cb_mutex, struct module *module);

参数net为网络设备名称,一般传入&init_net即可;unit为协议类型,传入自定义的协议类型;groups指定netlink 消息有多少个组,一般情况下传入0即可;input是用于netlink socket在收到消息时调用的处理消息的函数指针;cb_mutex是用于内核处理netlink socket消息时使用的互斥锁,一般情况下传入NULL即可;module指定创建的netlink socket所属的内核模块,一般情况下传入THIS_MODULE。

在内核创建了netlink socket后,当用户程序发送一个netlink消息到内核时,回调函数input都会被调用。下面是一个实现了消息处理函数input的例子:

void input (struct sock *sk, int len){struct sk_buff *skb;struct nlmsghdr *nlh = NULL;u8 *payload = NULL;while ((skb = skb_dequeue(&sk->receive_queue))!= NULL){/* process netlink message pointed by skb->data */nlh = (struct nlmsghdr *)skb->data;payload = NLMSG_DATA(nlh);/ * process netlink message with header pointed by   * nlh  and payload pointed by payload  */}}

回调函数input是在用户进程调用sendmsg系统调用时被调用的。因此该函数中的处理时间不能太长,否则会导致其他系统调用被阻塞。比较好的做法是在该函数中创建一个新的内核线程来处理netlink socket消息。而函数input的工作只是唤醒该内核线程,这样sendmsg将很快返回。

函数skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收队列上的消息,返回为一个struct sk_buff的结构,skb->data指向实际的netlink消息。
函数skb_recv_datagram(nl_sk)也用于在netlink socket nl_sk上接收消息,与skb_dequeue的不同指出是,如果socket的接收队列上没有消息,

它将导致调用进程睡眠在等待队列 nl_sk->sk_sleep,因此它必须在进程上下文使用,刚才讲的内核线程就可以采用这种方式来接收消息。

下面的函数input就是这种使用的示例:

void input (struct sock *sk, int len){wake_up_interruptible(sk->sk_sleep);
}

当内核中发送netlink消息时,也需要设置目标地址与源地址,而且内核中消息是通过struct sk_buff来管理的, linux/netlink.h中定义了一个宏来方便消息的地址设置。

#define NETLINK_CB(skb) ((struct netlink_skb_parms)&((skb)->cb))

下面是一个消息地址设置的例子:

NETLINK_CB(skb).pid = 0;

NETLINK_CB(skb).dst_pid = 0;

NETLINK_CB(skb).dst_group = 1;

字段pid表示消息发送者进程ID,也即源地址,对于内核,它为 0, dst_pid 表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0,否则 dst_group 表示目标组地址,如果它目标为某一进程或内核,dst_group 应当设置为 0。

3) 在内核中发送netlink消息

在内核空间发送netlink消息时有两个接口可以使用,netlink_unicast接口用来发送一个单播消息,其定义如下:

int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)

ssk是调用netlink_kernel_create接口所创建的socket控制块,为函数netlink_kernel_create()返回的socket;

skb中的data指向需要发送的netlink消息体,而skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块

pid为要发往的用户进程的PID;参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠

netlink消息的源地址和目的地址可以通过如下代码进行设置:

NETLINK_CB(skb).groups = local_groups;NETLINK_CB(skb).pid = 0;   /* from kernel */NETLINK_CB(skb).dst_groups = dst_groups;NETLINK_CB(skb).dst_pid = dst_pid;

使用netlink_broadcast接口可以发送一个多播消息,其定义如下:

int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)

其中group是所有目标多播组对应掩码进行或运算的结果。allocation是内核申请内存的类型,通常情况下在中断上下文中使用GFP_ATOMIC(即不可以睡眠),否则使用GFP_KERNEL。

4)在内核中关闭netlink socket

假设netlink_kernel_create函数返回的netlink socket为struct sock *nl_sk,可以通过API来关闭这个netlink socket,注意函数netlink_kernel_create()返回的类型为struct sock,因此函数sock_release应该这种调用 :sock_release(nl_sk->socket); nl_sk为函数netlink_kernel_create()的返回值。

四、netlink子系统初始化

内核Netlink的初始化在系统启动阶段完成,初始化代码在af_netlink.c的netlink_proto_init()函数中,整个初始化流程如下:

netlink_table结构体:

struct netlink_table {struct rhashtable   hash;  //用来索引同种协议类型的不同netlink套接字实例struct hlist_head   mc_list;//多播使用的sock散列表struct listeners __rcu  *listeners; //监听者掩码unsigned int        flags;unsigned int        groups; //协议支持最大多播组数目struct mutex        *cb_mutex;struct module       *module;int         (*bind)(struct net *net, int group);void            (*unbind)(struct net *net, int group);bool            (*compare)(struct net *net, struct sock *sock);int         registered;};

netlink子系统初始化:

static int __init netlink_proto_init(void){int i;int err = proto_register(&netlink_proto, 0); //向内核注册netlink协议if (err != 0)goto out;BUILD_BUG_ON(sizeof(struct netlink_skb_parms) > FIELD_SIZEOF(struct sk_buff, cb));nl_table = kcalloc(MAX_LINKS, sizeof(*nl_table), GFP_KERNEL);//创建并初始化netlink_table表数组nl_table,每种协议占nl_table表数组的一项if (!nl_table)goto panic;for (i = 0; i < MAX_LINKS; i++) {if (rhashtable_init(&nl_table[i].hash,&netlink_rhashtable_params) < 0) {while (--i > 0)rhashtable_destroy(&nl_table[i].hash);kfree(nl_table);goto panic;}}

INIT_LIST_HEAD(&netlink_tap_all);

netlink_add_usersock_entry();//初始化应用层使用的NETLINK_USERSOCK协议类型的netlink(用于应用层进程间通信)
sock_register(&netlink_family_ops);//向内核注册协议处理函数,即将netlink的socket创建处理函数注册到内核中,如此以后应用层创建netlink类型的socket时将会调用该协议处理函数,

register_pernet_subsys(&netlink_net_ops);//向内核所有的网络命名空间注册”子系统“的初始化和去初始化函数,这里的"子系统”并非指的是netlink子系统,而是一种通用的处理方式,在网络命名空间创建和注销时会调用这里注册的初始化和去初始化函数(当然对于已经存在的网络命名空间,在注册的过程中也会调用其初始化函数),后文中创建各种协议类型的netlink也是通过这种方式实现的。

   /* The netlink device handler may be needed early. */rtnetlink_init();//创建NETLINK_ROUTE协议类型的netlinkout:return err;panic:panic("netlink_init: Cannot allocate nl_table\n");}core_initcall(netlink_proto_init);

netlink实现与使用方法详解(用户态/内核态)相关推荐

  1. linux oracle 用户创建,LINUX下Oracle数据库用户创建方法详解

    本文实例分析了LINUX下Oracle数据库用户创建方法.分享给大家供大家参考,具体如下: 1)登录linux,以oracle用户登录(如果是root用户登录的,登录后用 su - oracle命令切 ...

  2. mysql 当前用户连接数,查看mysql当前连接数的方法详解

    1.查看当前所有连接的详细资料: ./mysqladmin -uadmin -p -h10.140.1.1 processlist2.只查看当前连接数(Threads就是连接数.): ./mysqla ...

  3. 查看登陆系统用户的信息的三种方法详解

    查看登陆系统用户的信息的三种方法详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.who这个命令显示可以谁在登陆,但是这个有很多的花式玩法,这个命令超简单 语法:who [O ...

  4. php能做定时关机吗,window_win10怎么定时关机?Win10系统定时关机命令使用方法详解,对于电脑用户来说,定时关机 - phpStudy...

    win10怎么定时关机?Win10系统定时关机命令使用方法详解 对于电脑用户来说,定时关机有时候经常会用到,比如夜间下载东西或者下载一些大文件,而又没有时间一直守着电脑,这时候设置电脑定时关机就很实用 ...

  5. Spring JdbcTemplate方法详解

    2019独角兽企业重金招聘Python工程师标准>>> Spring JdbcTemplate方法详解 标签: springhsqldbjava存储数据库相关sql 2012-07- ...

  6. mysql回滚用法_Mysql误操作后利用binlog2sql快速回滚的方法详解

    前言 在日常工作或者学习中,操作数据库时候难免会因为"大意"而误操作,需要快速恢复的话通过备份来恢复是不太可能的,下面这篇文章主要给大家介绍关于Mysql误操作后利用binlog2 ...

  7. pycharm 运行celery_在 Pycharm 安装使用black的方法详解

    PyCharm是一种Python IDE,带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具,比如调试.语法高亮.Project管理.代码跳转.智能提示.自动完 成.单元测试.版本控制 ...

  8. MP实战系列(十一)之封装方法详解(续一)

    之前写的封装方法详解,比较简要. 今天我主要讲增加和删除及其修改.查的话得单独再详讲. 增删改查,无论是Java或者C#等等,凡是对数据库操作的都离不开这四个. 一.增加方法讲解 MyBatis Pl ...

  9. python连接oracle数据库的方法_Python3.6连接Oracle数据库的方法详解

    本文实例讲述了Python3.6连接Oracle数据库的方法.分享给大家供大家参考,具体如下: 下载cx_Oracle模块模块: https://pypi.python.org/pypi/cx_Ora ...

最新文章

  1. maven学习笔记之IDEA+Maven+Jetty运行一个简单的web项目
  2. [C#学习] DataAdapter.Fill()分页
  3. 用事件驱动编程解救臃肿的代码
  4. 前端实现照片选取范围上传_前端代码是怎样智能生成的?看看大佬怎么说
  5. 制作扫码连wifi的二维码
  6. 常用的linux技巧,Linux Shell常用技巧(十二)-第二部分
  7. linux只能单向ping通,两机只能单向Ping通的原因、ping的原理
  8. ios用xib实现三等分以及多等分思路
  9. 2个最好的中文图标搜索引擎
  10. 绘图软件教程(2)coreldraw12图层的使用与DXF文件的导出
  11. 电动汽车的新问题,保险公司赔不起,车主养不起
  12. 【gitLab】gitLab新建项目步骤
  13. Django+Vue实现增删改查操作
  14. MISRA C (3)
  15. linux删除的快捷键
  16. php5 dm.dll,win10模块dll加载失败二进制怎么解决?
  17. matlab单位采样序列程序,常用序列的MATLAB代码(一)
  18. 上传文件到github上的两种方式
  19. RF高速电路的电源走线方式
  20. Android工程中imei和oaid的获取

热门文章

  1. python保存路径相对路径_Python绝对路径和相对路径详解
  2. bootstrap fileinput上传组件和form表单一起提交
  3. 爽爽的贵阳,楼市被“大数据”炒得这么热!
  4. 【vue】在vue项目中按顺序动态24个英文字母选项:A B C D E F......
  5. python自制简易二维码代码_Python用5行代码写一个自定义简单二维码
  6. 5年客户端开发的程序员如何转型服务端开发?
  7. 淘宝/天猫API:item_search_samestyle-搜索同款的商品
  8. matlab构建一个神经网络,matlab构建神经网络
  9. oracle10在w7下提示无法定位,Win7提示无法定位程序输入点 于动态链接库的原因及解决方法...
  10. 触发器_差之毫厘谬以千里