virtio前端驱动详解
2016-11-08
前段时间大致整理了下virtIO后端驱动的工作模式以及原理,今天就从前端驱动的角度描述下目前Linux内核代码中的virtIO驱动是如何配合后端进行工作的。
注:本节代码参考Linux 内核3.11.1代码
virtIO驱动从架构上来讲可以分为两部分,一个是其作为PCI设备本身的驱动,此驱动需要提供一些基本的操作PCI设备本身的函数比如PCI设备的探测、删除、配置空间的设置和寄存器空间的读写等。而另一个就是其virtIO设备本身实现的功能驱动例如网络驱动、块设备驱动、console驱动等。所以我们要看还是分两部分,先介绍PCI设备本身的驱动,然后在介绍实际功能驱动。
一、PCI设备本身驱动
在前面的PCI系列文章中对Linux内核中PCI设备驱动做了分析,所以这里我们只分析和virtIO相关的部分。
二、功能驱动部分
其实大部分的功能在后端驱动已经介绍,只是有些功能是在前端实现的,比如说virtqueue的初始化、avail buffer的添加以及used buffer的消费,还有比较很重要的是前后端vring的同步。
鉴于前面已经有了基本的概念基础,那么我们直接从网络驱动下手,分析驱动从注册到接受数据的整个流程。(参考代码virtio-net.c)
看下网络驱动注册的操作函数:
static const struct net_device_ops virtnet_netdev = {
.ndo_open = virtnet_open,
.ndo_stop = virtnet_close,
.ndo_start_xmit = start_xmit,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = virtnet_set_mac_address,
.ndo_set_rx_mode = virtnet_set_rx_mode,
.ndo_change_mtu = virtnet_change_mtu,
.ndo_get_stats64 = virtnet_stats,
.ndo_vlan_rx_add_vid = virtnet_vlan_rx_add_vid,
.ndo_vlan_rx_kill_vid = virtnet_vlan_rx_kill_vid,
.ndo_select_queue = virtnet_select_queue,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = virtnet_netpoll,
#endif
};
发送数据的函数为start_xmit,该函数接收来自网络协议栈的函数并写入到ring buffer中,然后通知后端驱动。
static netdev_tx_t start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct virtnet_info *vi = netdev_priv(dev);
int qnum = skb_get_queue_mapping(skb);
struct send_queue *sq = &vi->sq[qnum];
int err; /* Free up any pending old buffers before queueing new ones. */
free_old_xmit_skbs(sq); /* Try to transmit */
err = xmit_skb(sq, skb); /* This should not happen! */
if (unlikely(err)) {
dev->stats.tx_fifo_errors++;
if (net_ratelimit())
dev_warn(&dev->dev,
"Unexpected TXQ (%d) queue failure: %d\n", qnum, err);
dev->stats.tx_dropped++;
kfree_skb(skb);
return NETDEV_TX_OK;
}
/*通知后端驱动*/
virtqueue_kick(sq->vq); /* Don't wait up for transmitted skbs to be freed. */
skb_orphan(skb);
nf_reset(skb); /* Apparently nice girls don't return TX_BUSY; stop the queue
* before it gets out of hand. Naturally, this wastes entries. */
if (sq->vq->num_free < +MAX_SKB_FRAGS) {
netif_stop_subqueue(dev, qnum);
if (unlikely(!virtqueue_enable_cb_delayed(sq->vq))) {
/* More just got used, free them then recheck. */
free_old_xmit_skbs(sq);
if (sq->vq->num_free >= +MAX_SKB_FRAGS) {
netif_start_subqueue(dev, qnum);
virtqueue_disable_cb(sq->vq);
}
}
}
return NETDEV_TX_OK;
}
函数中首先获取了buffer对应的发送队列sendqueue,调用了一个关键的函数xmit_skb,具体的添加buffer到queue中的操作就是在此函数实现的:
static int xmit_skb(struct send_queue *sq, struct sk_buff *skb)
{
struct skb_vnet_hdr *hdr = skb_vnet_hdr(skb);
const unsigned char *dest = ((struct ethhdr *)skb->data)->h_dest;
struct virtnet_info *vi = sq->vq->vdev->priv;
unsigned num_sg; pr_debug("%s: xmit %p %pM\n", vi->dev->name, skb, dest); if (skb->ip_summed == CHECKSUM_PARTIAL) {
hdr->hdr.flags = VIRTIO_NET_HDR_F_NEEDS_CSUM;
hdr->hdr.csum_start = skb_checksum_start_offset(skb);
hdr->hdr.csum_offset = skb->csum_offset;
} else {
hdr->hdr.flags = ;
hdr->hdr.csum_offset = hdr->hdr.csum_start = ;
} if (skb_is_gso(skb)) {
hdr->hdr.hdr_len = skb_headlen(skb);
hdr->hdr.gso_size = skb_shinfo(skb)->gso_size;
if (skb_shinfo(skb)->gso_type & SKB_GSO_TCPV4)
hdr->hdr.gso_type = VIRTIO_NET_HDR_GSO_TCPV4;
else if (skb_shinfo(skb)->gso_type & SKB_GSO_TCPV6)
hdr->hdr.gso_type = VIRTIO_NET_HDR_GSO_TCPV6;
else if (skb_shinfo(skb)->gso_type & SKB_GSO_UDP)
hdr->hdr.gso_type = VIRTIO_NET_HDR_GSO_UDP;
else
BUG();
if (skb_shinfo(skb)->gso_type & SKB_GSO_TCP_ECN)
hdr->hdr.gso_type |= VIRTIO_NET_HDR_GSO_ECN;
} else {
hdr->hdr.gso_type = VIRTIO_NET_HDR_GSO_NONE;
hdr->hdr.gso_size = hdr->hdr.hdr_len = ;
} hdr->mhdr.num_buffers = ; /* Encode metadata header at front. 首个sg entry存储头部信息*/
if (vi->mergeable_rx_bufs)
sg_set_buf(sq->sg, &hdr->mhdr, sizeof hdr->mhdr);
else
sg_set_buf(sq->sg, &hdr->hdr, sizeof hdr->hdr);
/*映射数据到sg,当前在sq->sg里面已经记录数据的地址信息了*/
num_sg = skb_to_sgvec(skb, sq->sg + , , skb->len) + ;
/*调用函数把sg 信息记录到队列中的desc中*/
return virtqueue_add_outbuf(sq->vq, sq->sg, num_sg, skb, GFP_ATOMIC);
}
这里我们先介绍下两种virtIO 头部:
struct skb_vnet_hdr {
union {
struct virtio_net_hdr hdr;
struct virtio_net_hdr_mrg_rxbuf mhdr;
};
};
里面包含一个union分别是virtio_net_hdr和virtio_net_hdr_mrg_rxbuf,前者是普通的数据包头部,后者是支持合并buffer的数据包的头部,并且virtio_net_hdr是virtio_net_hdr_mrg_rxbuf的一个内嵌结构,这样再看前面的函数代码
首先判断硬件是否已经添加了校验字段,设置virtio_net_hdr中相关的值;然后判断数据包是否是GSO类型,再次设置virtio_net_hdr相关字段的值。关于GSO类型,文章最后会介绍。设置好头部后,进入下一个if,判断设备是否支持合并buffer,是的话就调用函数sg_set_buf把virtio_net_hdr_mrg_rxbuf记录到首个sg table的第一个表项 中,否则添加virtio_net_hdr。这样设置好头部,就调用skb_to_sgvec函数把skb buffer记录到sg table中,然后调用virtqueue_add_outbuf把sg table转换到发送队列的ring desc中。回到上层的函数start_xmit中,在xmit_skb返回后,如果返回值正常,就调用virtqueue_kick函数通知后端驱动。
在通知后端驱动后判断剩余可用的desc是否小于2+MAX_SKB_FRAGS(为保证安全,一个数据包最多可能使用2+MAX_SKB_FRAGS个物理buffer,virtIO 头部占用一个,数据包头部占用一个,剩下的是数据包最大分片数),不小于的话需要调用netif_stop_subqueue禁止下一个数据包的发送。
下面回过头分析sg_set_buf、skb_to_sgvec和virtqueue_add_outbuf。
static inline void sg_set_buf(struct scatterlist *sg, const void *buf,
unsigned int buflen)
{
sg_set_page(sg, virt_to_page(buf), buflen, offset_in_page(buf));
}
在分析的同时我们也看下scatter list是如何组织的。首先看参数sg是sg table 的指针,buf指向数据,buflen是数据的长度。可以看到函数中仅仅是调用了sg_set_page函数,所以这里具体的物理buffer块是按照页为单位的。由于buf并不一定是页对齐的,所以需要一个buf指针到所在页基址的偏移。
static inline void sg_set_page(struct scatterlist *sg, struct page *page,
unsigned int len, unsigned int offset)
{
sg_assign_page(sg, page);
sg->offset = offset;//data在页面中的偏移
sg->length = len;//data的长度
}
到该函数中,page是一个指向一个页的指针,该函数中调用了sg_assign_page函数设置sg->page_link指向page,这样在sg table entry和具体的buffer就联系起来了。然后把buffer的offset和length记录到sg entry中。
结合xmit_skb函数,那么在经过sg_set_buffer之后,sg table的第一个表项便和hdr->mhdr或者hdr->hdr联系起来。
Function skb_to_sgvec
int skb_to_sgvec(struct sk_buff *skb, struct scatterlist *sg, int offset, int len)
{
/*buffer数据存储在sg的个数*/
int nsg = __skb_to_sgvec(skb, sg, offset, len);
/*标记最后一个sg entry结束*/
sg_mark_end(&sg[nsg - ]); return nsg;
}
该函数直接调用了__skb_to_sgvec函数,有其实现具体的功能,然后设置最后一个entry为end end entry,以此表明sg list的结束。
static int
__skb_to_sgvec(struct sk_buff *skb, struct scatterlist *sg, int offset, int len)
{
int start = skb_headlen(skb);
int i, copy = start - offset;
struct sk_buff *frag_iter;
int elt = ;/*elt记录sg entry的个数*/ if (copy > ) {/*copy是头部的长度*/
if (copy > len)/*头部大于总长度。。。几乎不可能*/
copy = len;
/*skb->data + offset是数据起始位置,尽管offset一般是0,所以可以看出头部是占用一个sg entry*/
sg_set_buf(sg, skb->data + offset, copy);
elt++;
if ((len -= copy) == )
return elt;
/*offset记录数据copy的位置*/
offset += copy;
}
/*映射非线性数据即skb_shared_info相关的数据*/
for (i = ; i < skb_shinfo(skb)->nr_frags; i++) {
int end; WARN_ON(start > offset + len); end = start + skb_frag_size(&skb_shinfo(skb)->frags[i]);
if ((copy = end - offset) > ) {
skb_frag_t *frag = &skb_shinfo(skb)->frags[i]; if (copy > len)
copy = len;
sg_set_page(&sg[elt], skb_frag_page(frag), copy,
frag->page_offset+offset-start);
elt++;
if (!(len -= copy))
return elt;
offset += copy;
}
start = end;
} skb_walk_frags(skb, frag_iter) {
int end; WARN_ON(start > offset + len); end = start + frag_iter->len;
if ((copy = end - offset) > ) {
if (copy > len)
copy = len;
elt += __skb_to_sgvec(frag_iter, sg+elt, offset - start,
copy);
if ((len -= copy) == )
return elt;
offset += copy;
}
start = end;
}
BUG_ON(len);
return elt;
}
该函数把一个完整的skbuffer记录到sg table,要搞清楚这些最好对sk_buffer结构比较清楚,而对sk_buffer结构可以参考其的有关专门的介绍。本节我们只介绍相关的部分,这里可以把skbuffer分成两部分:
1、skbuffer本身的数据
2、skb_shared_info记录的分片数据
而上面的函数也是把这两部分分开记录的,首先调用skb_headlen函数获取sk_buffer本身的头部以及数据(不包含分片数据),copy为实际的长度,不过这里传递进来的offset为0,所以copy即start,接着就调用了sg_set_buf函数把从skb_buffer->data+offset起始的有效数据记录到sg table,elt是一个变量记录使用的sg entry个数。
如果这里没有分片数据,那么直接返回elt,否则需要记录offset的位置,便于下次知道上次数据的记录位置。
下面一个for循环时完成第二部分数据的记录,即分片数据。sk_buffer->end指向一个skb_shared_info结构,该结构管理分片数据,nr_frags表示分片的数量,所以以此为基添加分片。
循环内部的内容有点混乱感觉,这里详解解释下:
注意一下几个变量:
/*
*len是未复制的数据的长度
*offset是已经复制的数据的长度
*copy是本次要复制的数据的长度
*start 是线性数据段的长度
*映射非线性数据即skb_shared_info相关的数据
*/
其实说实话我个人觉得这几个变量的命名很是失败,start和end咋一看容易让人感觉这是指针,但是没办法,说让咱写不出这种代码勒!
在循环之前,start是代表线性数据段的长度,offset在完成映射后就执行offset += copy,所以offset=start。
在循环中end=start +分片size,copy=end-offset,那么实际上,本次copy的长度也就是分片的size。如果分片size大于0,则表示分片存在数据,那么copy=end-offset必定大于0,直接调用sg_set_page函数把当前分片扩展成一个page然后映射到sg table.接着更新len,判断是否映射完成,即len是否为0,不为0的话更新offset。最后更新start=end.
下面遍历所有的sk_buffer->frag_list,对于每个sk_buffer,都调用__skb_to_sgvec对其中数据进行映射,最后返回elt即使用的sg entry个数。
/*关于sk_buffer,确实其组织方式很复杂,会单独讲解,碍于篇幅,就不在这里详细描述*/
回到skb_to_sgvec函数中,调用sg_mark_end对最后一个entry做末端标记。具体而言就是设置sg->page_link第二位为1:sg->page_link |= 0x02;
到这里就把buffer映射到了sg table中。那么如何把sg填入ring desc数组中呢?看virtqueue_add_outbuf
这里需要注意一下传入的data指针是skb,即数据的虚拟起始地址
int virtqueue_add_outbuf(struct virtqueue *vq,
struct scatterlist sg[], unsigned int num,
void *data,
gfp_t gfp)
{
return virtqueue_add(vq, &sg, sg_next_arr, num, , , , data, gfp);
}
这里就是简单的调用了下virtqueue_add
static inline int virtqueue_add(struct virtqueue *_vq,
struct scatterlist *sgs[],
struct scatterlist *(*next)
(struct scatterlist *, unsigned int *),
unsigned int total_out,
unsigned int total_in,
unsigned int out_sgs,
unsigned int in_sgs,
void *data,
gfp_t gfp)
{
struct vring_virtqueue *vq = to_vvq(_vq);
struct scatterlist *sg;
unsigned int i, n, avail, uninitialized_var(prev), total_sg;
int head; START_USE(vq); BUG_ON(data == NULL); #ifdef DEBUG
{
ktime_t now = ktime_get(); /* No kick or get, with .1 second between? Warn. */
if (vq->last_add_time_valid)
WARN_ON(ktime_to_ms(ktime_sub(now, vq->last_add_time))
> );
vq->last_add_time = now;
vq->last_add_time_valid = true;
}
#endif total_sg = total_in + total_out;
//这里判断是否支持间接描述符并且总的entry数要大于1且,vring里至少有一个空buffer
/* If the host supports indirect descriptor tables, and we have multiple
* buffers, then go indirect. FIXME: tune this threshold */
if (vq->indirect && total_sg > && vq->vq.num_free) {
head = vring_add_indirect(vq, sgs, next, total_sg, total_out,
total_in,
out_sgs, in_sgs, gfp);
if (likely(head >= ))//如果执行成功,就直接执行add_head段
goto add_head;
}
/*否则就可能是不支持间接描述符,那么这是需要有足够的desc来装载哪些entry*/ BUG_ON(total_sg > vq->vring.num);
BUG_ON(total_sg == );
/*如果可用的desc数量不够,则不能执行成功*/
if (vq->vq.num_free < total_sg) {
pr_debug("Can't add buf len %i - avail = %i\n",
total_sg, vq->vq.num_free);
/* FIXME: for historical reasons, we force a notify here if
* there are outgoing parts to the buffer. Presumably the
* host should service the ring ASAP. */
if (out_sgs)
vq->notify(&vq->vq);
END_USE(vq);
return -ENOSPC;
} /* We're about to use some buffers from the free list. */
vq->vq.num_free -= total_sg;
/*可用的desc数量够的话就可以直接使用这些desc,针对desc的操作都是一样的*/
head = i = vq->free_head;
for (n = ; n < out_sgs; n++) {
for (sg = sgs[n]; sg; sg = next(sg, &total_out)) {
vq->vring.desc[i].flags = VRING_DESC_F_NEXT;
vq->vring.desc[i].addr = sg_phys(sg);
vq->vring.desc[i].len = sg->length;
prev = i;
i = vq->vring.desc[i].next;
}
}
for (; n < (out_sgs + in_sgs); n++) {
for (sg = sgs[n]; sg; sg = next(sg, &total_in)) {
vq->vring.desc[i].flags = VRING_DESC_F_NEXT|VRING_DESC_F_WRITE;
vq->vring.desc[i].addr = sg_phys(sg);
vq->vring.desc[i].len = sg->length;
prev = i;
i = vq->vring.desc[i].next;
}
}
/* Last one doesn't continue. */
vq->vring.desc[prev].flags &= ~VRING_DESC_F_NEXT; /* Update free pointer */
vq->free_head = i; add_head:
/* Set token. */
/*在客户机驱动写入数据到buffer以后,设置data数组以head为下标的内容为buffer的虚拟地址*/
vq->data[head] = data; /* Put entry in available array (but don't update avail->idx until they
* do sync). */ //然后把本次传送所用到的描述符表的信息写入avail结构中
/*&应该是要保证idx小于vq->vring.num*/
avail = (vq->vring.avail->idx & (vq->vring.num-));
/*设置avail_ring*/
vq->vring.avail->ring[avail] = head; /* Descriptors and available array need to be set before we expose the
* new available array entries. */
virtio_wmb(vq->weak_barriers);
vq->vring.avail->idx++;
vq->num_added++; /* This is very unlikely, but theoretically possible. Kick
* just in case. */
if (unlikely(vq->num_added == ( << ) - ))
virtqueue_kick(_vq); pr_debug("Added buffer head %i to %p\n", head, vq);
END_USE(vq); return ;
}
该函数实现了把sg table中记录的信息,复制到发送队列的ring的desc数组中。看下该函数几个重要的参数:
struct virtqueue *_vq 添加的目的队列
struct scatterlist *sgs[] 要添加的sg table
struct scatterlist *(*next) 一个函数指针,用于获取下一个sg entry
(struct scatterlist *, unsigned int *)
unsigned int total_out 输出的sg entry的个数
unsigned int total_in 输入的sg entry的个数
unsigned int out_sgs 输出的sg list的个数,这里一个out_sgs代表一个完整的skb_buffer
unsigned int in_sgs 输入的sg list的个数,这里一个in_sgs代表一个完整的skb_buffer
void *data 一个指向sk_buffer的指针。
介绍完这些,下面的就很明确了,
首先判断是否支持队列是否支持indirect descriptor,首选也是使用这种方式,不过这种方式需要占用主描述符表的一个表项,并且在total_sg>1的时候使用(total_sg=1时只是使用主描述符表即可),如果满足条件就调用vring_add_indirect函数添加间接描述符表,并把sg table中记录的信息写入到描述符表中。
如果不支持,就只能使用主描述符表,此时主描述符表的空闲表项数必须大于等于total_sg,具体可用的数目记录在vq->vq.num_free,而首个可用的表项的下标记录在vq->free_head中,下面的for循环就依次把sg table中entry的信息记录到对应的desc表中,需要注意的是desc中的addr记录的是buffer的物理地址,而sg是记录的页虚拟地址。下面的一个for循环是添加in_sg,关于in_sg和out_sg,目前在网络驱动部分的发送队列只使用out_sg,而接受队列只使用in_sg,而控制队列就可能两个都使。这里我们忽略此点即可。
最后依然需要设置最后一个desc为末端desc,并移动vq->free_head便于下次使用。
add_head后面的部分是和后端驱动相关的。
主要是在发送队列的ring[]中获取一项,写入前面写入的sk_buffer 对应的desc表中 的head,即首个描述符的下标。然后更新vq->vring.avail->idx。
到现在前端驱动已经设置完成,剩下就要通知后端驱动读取数据了,回到start_xmit函数中,看到调用了virtqueue_kick函数
void virtqueue_kick(struct virtqueue *vq)
{
if (virtqueue_kick_prepare(vq))
virtqueue_notify(vq);
}
virtqueue_kick函数调用了virtqueue_kick_prepare判断下当前是否应该notify后端,如果应该,就调用virtqueue_notify函数,该函数直接调用了vq->notify函数,参数是队列指针。
而具体实现的是下面的函数vp_notify在virtio_pci.c中
static void vp_notify(struct virtqueue *vq)
{
struct virtio_pci_device *vp_dev = to_vp_device(vq->vdev); /* we write the queue's selector into the notification register to
* signal the other end */
iowrite16(vq->index, vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY);
}
可以看到,实际上前端通知后端仅仅是把队列的索引写入到对应的设备寄存器中,这样在后端qemu就会知道是哪个队列发生了add buffer,然后就从对应队列的buffer取出数据。
而对于前后端notify机制的分析,这里我们单独拿出一节来讲,感兴趣可以参考:
参考:
- LInux 3.11.1内核源代码
- qemu 2.7.0源代码
- qemu 开发者的帮助
virtio前端驱动详解的更多相关文章
- virtio后端驱动详解
2016-10-08 virtIO是一种半虚拟化驱动,广泛用于在XEN平台和KVM虚拟化平台,用于提高客户机IO的效率,事实证明,virtIO极大的提高了VM IO 效率,配备virtIO前后端驱动的 ...
- linux usb 驱动详解
linux usb 驱动详解 USB 设备驱动代码通过urb和所有的 USB 设备通讯.urb用 struct urb 结构描述(include/linux/usb.h ). urb 以一种异步的方式 ...
- 25.Linux-Nor Flash驱动(详解)
1.nor硬件介绍: 从原理图中我们能看到NOR FLASH有地址线,有数据线,它和我们的SDRAM接口相似,能直接读取数据,但是不能像SDRAM直接写入数据,需要有命令才行 1.1其中我们2440的 ...
- 16.Linux-LCD驱动(详解)
在上一节LCD层次分析中,得出写个LCD驱动入口函数,需要以下4步: 1) 分配一个fb_info结构体: framebuffer_alloc(); 2) 设置fb_info 3) 设置硬件相关的操作 ...
- 使用VS2010编译MongoDB C++驱动详解
最近为了解决IM消息记录的高速度写入.多文档类型支持的需求,决定使用MongoDB来解决. 考虑到MongoDB对VS版本要求较高,与我现有的VS版本不兼容,在leveldb.ssdb.redis.h ...
- 16.Linux-LCD驱动(详解)【转】
转自:https://www.cnblogs.com/lifexy/p/7604011.html 在上一节LCD层次分析中,得出写个LCD驱动入口函数,需要以下4步: 1) 分配一个fb_info结构 ...
- 13.Linux键盘驱动 (详解)
版权声明:本文为博主原创文章,未经博主允许不得转载. 在上一节分析输入子系统内的intput_handler软件处理部分后,接下来我们开始写input_dev驱动 本节目标: 实现键盘驱动,让开发板的 ...
- 18.Llinux-触摸屏驱动(详解)
本节的触摸屏驱动也是使用之前的输入子系统 1.先来回忆之前第12节分析的输入子系统 其中输入子系统层次如下图所示, 其中事件处理层的函数都是通过input_register_handler()函数注册 ...
- 21.Linux-写USB键盘驱动(详解)
本节目的: 根据上节写的USB鼠标驱动,来依葫芦画瓢写出键盘驱动 1.首先我们通过上节的代码中修改,来打印下键盘驱动的数据到底是怎样的 先来回忆下,我们之前写的鼠标驱动的id_table是这样: 所以 ...
随机推荐
- 超赞的lua开发工具zerobrane
zerobrane是用lua和wxWidgets编写的ide,而且是跨平台的,支持多种lua解释器,包括love2d. 而且最赞的是支持即时编程,可以在运行时直接修改变量,直接看到结果,不用重新运行, ...
- [初学WPF]控件大小自适应
想在Win上自己写点小工具用,GUI自然是免不了的,于是决定学一学WPF,直接拖控件是很方便啊.控件拖出来以后发现运行时改变窗口大小控件不会重绘,搜索了一下发现了解决办法:使用Viewbox控件. V ...
- xeno 实时性能测试 在100us的采样周期的测试数据
1 xeno 用户层测试时间: root@sama5d3-linux:/usr/bin latency -t0 -T25 -p100== Sampling period: 100 us== Test ...
- 以下web.xml片断( )正确地声明servlet 上下文参数。
A <init-param> <param-name>MAX</param-name> <param-value>100</param-value ...
- ES 中的 POST 和 PUT 的区别
0.什么是Restful架构 比较难说清楚,这部分大雾状态,引ruanyf 理解RESTful架构 的几句总结吧: Fielding将他对互联网软件的架构原则,定名为REST,即Representat ...
- HBaseclientAPI基本操作
Java类与HBase数据模型 HBaseConfiguration 包名 : org.apache.hadoop.hbase.HBaseConfiguration 作用:对HBase进行配置. 使用 ...
- Administrator privileges required for OLE Remote Procedure Call debugging: this feature will not work..
VC++ 6.0单步调试(F10)出现警告框: 解决方法: 右键VC++ 6.0程序图标
- Python爬虫(八)
源码: import requests import re from my_mysql import MysqlConnect import time,random # 获取招聘详情链接 def ge ...
- AWS系列-EC2默认限制说明
Amazon EC2 提供您可以使用的不同资源,例如实例和卷. 在您创建 AWS 账户时,AWS 会针对每个区域中的这些资源设置限制.此页面列出您在 亚太区域 (东京) 中的 EC2 服务限制. 1. ...
- android classloader双亲托付模式
概述 ClassLoader的双亲托付模式:classloader 按级别分为三个级别:最上级 : bootstrap classLoader(根类载入器) : 中间级:extension class ...