Xenomai 源码分析-Part I
Xenomai Edition v3.0.5
xenomai_init()
static int __init xenomai_init(void)
源码分析
setup_init_state
// 配置Xenomai为启动状态
CONFIG_SMP
// 判断处理器CPU系统架构是否为 SMP,是则对每一个CPU进行处理,设置标志位
xnsched_register_classes();
// Linux Domain 调用流程结构初始化,重点关注 xnsched_class_rt
实时线程调用相关的回调函数参数
ret = xnprocfs_init_tree();
// 在 proc 文件夹下建立 Xenomai 文件夹
ret = mach_setup();
// 对系统的信息、时钟频率、设备的Pipeline底层进行初始化管理、底层硬件的匹配-初始化最底层中断信号等操作的传输方式 pipeline,配置了 对应底层设备中断信号的处理函数,通过取得的 CPU 时钟参数设置 Xenomai 运行的时钟周期
ret = ipipe_select_timers(&xnsched_realtime_cpus);
// 系统时钟配置ipipe_get_sysinfo(&sysinfo);
// 获取系统时钟、频率参数 赋值非全局变量cobalt_pipeline.timer_freq = timerfreq_arg;
// 使用更新后的全局变量对 Cobalt PipeLine 进行初始化if (cobalt_machine.init)
// 对于某些特定的机器 CPU 需要初始化的进行初始化操作virq = ipipe_alloc_virq();
// 通过系统底层pipe服务申请irq中断,绑定到实时核 Xenomai 的资源上ret = xnclock_init(cobalt_pipeline.clock_freq);
// 初始化 Xenomai 的系统时钟xnclock_update_freq(freq);
// 从Pipe服务底层获取自旋锁读取CPU时钟频率nktimerlat = xnarch_timer_calibrate();
// 通过各个机器CPU自带的标准矫正函数对时钟频率进行多次测算求平均得到准确的时钟xnclock_reset_gravity(&nkclock);
// 根据机器配置设置latence补偿xnclock_register(&nkclock, &xnsched_realtime_cpus);
// 生效上述时钟配置,同时在/proc/xenomai/
文件夹下建立 CPU 相关文件
xnintr_mount();
// 中断向量初始化
ret = xnpipe_mount();
// Xenomai 管道初始化、加载、创建pipe管道设备(device_create)、注册字符设备(register_chrdev)
root@MM5718v1:~# ls -al /dev/rtp1
crw------- 1 root root 150, 1 Dec 26 08:14 /dev/rtp1
ret = xnselect_mount();
//
ret = sys_init();
// Xenomai 系统初始化流程
sysheap_size_arg = CONFIG_XENO_OPT_SYS_HEAPSZ;
// 从配置文件中获取系统 heap 的大小heapaddr = xnheap_vmalloc(sysheap_size_arg * 1024);
// 从Linux底层申请获得指定大小的 heap 堆区,实际上获取的内存来源于 ZONE_NORMAL 区域,我们需要的内存区域的物理地址并不要求其是连续的xnheap_init(&cobalt_heap, heapaddr, sysheap_size_arg * 1024)
// 将上述申请到的堆区地址进行初始化size > XNHEAP_MAXHEAPSZ || !IS_ALIGNED(size,XNHEAP_PAGESZ)
// 分配的内存大小必须小于最大的Heap大小 2Gb = 256MB 且 必须是 PAGE_SIZE 的整数倍struct xnheap *heap
// 初始化堆区管理对象 xnheap 的成员变量heap->pagemap = kmalloc(sizeof(struct xnpagemap) * heap->npages,GFP_KERNEL);
// 为管理 heap 页创建对应数量的 pagemap 用于heap管理init_freelist(heap);
// 初始化配置 heap 管理结构体 pagemap,这里还有一个重要的流程就是将heap->freelist
初始化,如下所示:init_freelist
注:/* Mark each page as free in the page map. */
for (n = 0, freepage = heap->membase;
n < lastpgnum; n++, freepage += XNHEAP_PAGESZ) {
*((caddr_t *)freepage) = freepage + XNHEAP_PAGESZ;
heap->pagemap[n].type = XNHEAP_PFREE;
heap->pagemap[n].bcount = 0;
}
...
heap->freelist = heap->membase;
*((caddr_t *)freepage) = freepage + XNHEAP_PAGESZ;
这里初始化freelist
的关键在于通过将一级指针转换为二级指针,并将内存下一页的首地址存储到当前二级指针指向的指针内容当中,从而完成空闲内存list
的延申,这个处理非常重要,在后续 rt_heap_create 的过程中需要用来取空闲页的首地址.
xnheap_set_name(&cobalt_heap, "system heap");
// 设置申请的Heap名称为 [system heap]xnsched_init(sched, cpu);
// 此处为系统实时线程底层支持管理的初始化部分struct xnthread_init_attr attr;
// 线程状态、名称、子线程、所属CPU-ID等资源管理对象定义struct xnsched_class *p;
// 线程初始化、入队列、出队列等其他操作回调函数链表封装for_each_xnsched_class(p)
// 对每一种类的 sched 进行循环初始化操作,对于 RT-sched 的初始化操作实际上为对创建好的 prio 优先级 map 进行初始化配置__xnthread_init
// 配置初始化线程根对象 sched->rootcb- 配置初始化线程管理模块的定时器
timer
,主要的目的是用来记录各个子线程相对于当前运行的总时长:xnstat_exectime_set_current(sched, &sched->rootcb.stat.account);
xnthread_init_root_tcb(&sched->rootcb);
// 初始化线程控制 TCB(Thread Control Block): 对象,\(TCB\) 仅包含了线程执行需要的 PC、SP、Condition Code、Data Register#ifdef CONFIG_XENO_OPT_WATCHDOG
// 这里可以配置选择是否启用看门狗
xnregistry_init();
// Xenomai 内核对象管理初始化,提供内核对象存储和快速检索registry_obj_slots = kmalloc(...
// 系统各个模块对象寄存器管理槽对象ret = xnvfile_init_dir("registry", ®istry_vfroot, &cobalt_vfroot);
// 创建 registry 的目录ret = xnvfile_init_regular("usage", &usage_vfile, ®istry_vfroot);
// 创建 usage 的文件,usage实际用来记录整个系统资源使用的情况root@MM5718v1:~# cat /proc/xenomai/registry/usage
18/2048
proc_apc = xnapc_alloc("registry_export", ®istry_proc_schedule, NULL);
// 申请一个APC slot, 用于注册关注当前sched中的程序的加载情况,在后面其他代码中实际上输出到 apc 文件当中.root@MM5718v1:~# cat /proc/xenomai/apc
APC CPU0
0: 0 (pipe_wakeup)
1: 0 (selector_list_destroy)
2: 0 (registry_export)
nr_object_entries = xnregistry_hash_size();
// 根据CONFIG_XENO_OPT_REGISTRY_NRSLOTS
编译参数获取对应的 hash 空间大小,这里的hash结果用来方便在cobalt系统中查找对应名称的内核对象、如:有名信号量(sem)、有名消息队列(mq)、进程间通讯 xddp、iddp 等object_index = kmalloc(sizeof(*object_index) *nr_object_entries, GFP_KERNEL);
// 获取对应 hash-size 大小的的 hlist_head 链表空间INIT_HLIST_HEAD(&object_index[n]);
// 初始化所有的链表空间xnsynch_init(®ister_synch, XNSYNCH_FIFO, NULL);
// 初始化底层 sched 的方式,链表等控制对象,实现的同步方式为 XNSYNCH_FIFO 队列方式,将 apc 的调用过程实现为逐步执行的过程。提供了线程与资源同步互斥管理的功能set_realtime_core_state(COBALT_STATE_RUNNING);
// 配置当前 Xenomai 的系统状态为运行状态,可以开始运行
ret = mach_late_setup();
// 针对特定的CPU机器,可以增加后续的初始化程序函数,用来适配该CPU的其他功能,相当于CPU功能使能的预留功能ret = rtdm_init();
// 初始化实时操作系统的设备管理模块xntree_init(&protocol_devices);
// 实际上就是使用 rb_root 红黑树数据结构对相关设备进行管理记录,从而提高系统查询设备相关参数的速度- 在设备 /dev 目录下创建rtdm设备节点,系统中如下所示:
root@MM5718v1:~# ls -al /dev/rtdm/
drwxr-xr-x 2 root root 180 Dec 30 23:32 .
drwxr-xr-x 14 root root 15780 Dec 30 23:32 ..
crw-rw---- 1 root root 235, 0 Dec 30 23:32 EtherCAT0
crw-rw---- 1 root root 243, 0 Dec 30 21:48 autotune
crw-rw---- 1 root root 246, 0 Dec 30 21:48 memdev-private
crw-rw---- 1 root root 246, 1 Dec 30 21:48 memdev-shared
crw-rw---- 1 root root 245, 0 Dec 30 21:48 memdev-sys
crw-rw---- 1 root root 241, 0 Dec 30 21:48 switchtest
crw-rw---- 1 root root 242, 0 Dec 30 21:48 timerbench
ret = cobalt_init();
// cobalt 内核初始化函数pthread_t ptid = pthread_self();
// 获取当前线程自身的线程 IDcobalt_default_condattr_init();
// 使用系统自带的条件变量模板进行变量属性的控制,确保创建条件变量是的属性与系统属性相同:static pthread_condattr_t cobalt_default_condattr; 方便 cobalt 内核后续初始化过程中的使用需求__cobalt_init();
// 初始化low_init();
// 底层的 cobalt 的内存映射等相关内容的初始化old_sigill_handler = signal(SIGILL, sigill_handler);
// 信号异常处理函数注册、ABI、XenomaiFeature等部分的初始化cobalt_check_features(f);
// 映射 /dev/mem 设备节点,如下所示root@MM5718v1:~# ls -al /dev/mem
crw-r----- 1 root kmem 1, 1 Dec 30 21:48 /dev/mem
cobalt_init_umm(f->vdso_offset);
// 映射cobalt设备内存区域节点到 /dev/rtdm/memdev-private 上。 注: #define map_umm(__name, __size_r) __map_umm("/dev/rtdm/" __name, __size_r) 函数用来映射字符驱动设备节点到内存地址空间上,【memory heaps mapped】pthread_once(&init_bind_once, init_bind);
// 映射内存区域到 /dev/rtdm/memdev-private , 这里注意 pthread_once 表示该线程调用的函数在系统当中只执行一次(其机理是通过 Mutex 互斥锁改变执行标志位从而确保线程函数仅执行一次.)init_loadup(vdso_offset);
// 初始化映射 /dev/rtdm/memdev-shared 节点, vdso_offset 为 cobalt 内核的起始偏移量
cobalt_init_current_keys();
// 创建 线程私有key键值ret = pthread_key_create(&cobalt_current_key, NULL);
// 创建 cobalt_current_key 键值,通过 int pthread_setspecific(pthread_key_t key, const void pointer) 函数将键值与制定的线程内部的全局变量指针进行绑定,从而实现不同线程操作相同名称的键值,实际修改了不同的全局变量的功能。
cobalt_ticks_init(f->clock_freq);
// cobalt 的基础时钟 ticks 初始化
sa.sa_sigaction = cobalt_sigdebug_handler;
// cobalt 核心调试启动过程的信号处理,可能造成的失败的原因包括了 [SIGDEBUG_NOMLOCK, SIGDEBUG_RESCNT_IMBALANCE, SIGDEBUG_MUTEX_SLEEP, SIGDEBUG_WATCHDOG]pthread_atfork(NULL, NULL, cobalt_fork_handler);
// 暂未执行相关代码cobalt_mutex_init();
// cobalt 核心的 mutex 锁参数属性模板初始化,保证在后续创建的锁的对象属性以此为模板,在 Xenomai 实时核心的 mutex lock 实际上是对 Linux native Mutex lock 原生锁的封装,增加了特定的属性。cobalt_thread_init();
// 初始化 cobalt 核心的线程基础属性参数、sched 线程调度优先级等cobalt_print_init();
// 初始化打印的缓冲区大小、缓冲区内存空间等
policy = SCHED_FIFO;
// 将当前主进程切换到 实时 cobalt 核上rtdm_fd_init();
// 初始化 rtdm 文件节点对象
rt_heap_create()
int rt_heap_create(RT_HEAP *heap, const char *name, size_t heapsz, int mode)
源码分析
if (heapsz == 0 || heapsz >= 1U << 31)
// 判断申请的heap内存的大小必须限制在 1Byte~2Gb 之间,不在范围内则返回输入参数无效返回值 [-EINVAL]
if (mode & ~(H_PRIO|H_SINGLE))
// 判断申请heap内存的模式是否正确,不正确则返回
hcb = xnmalloc(sizeof(*hcb));
// 从 cobalt 系统heap 内存中为 hcb 变量申请内存空间
xnheap_alloc(&cobalt_heap, size)
// 从系统内存对象 cobalt_heap 中申请 size 大小的内存空间size = ALIGN(size, XNHEAP_PAGESZ);
// 根据需要申请的内存空间的大小通过 ALIGN 函数进行【字节、块、页】对齐操作:- 当申请的内存大小大于一页
XNHEAP_PAGESZ
以上时,按照页对齐的方式分配内存空间,size = k*XNHEAP_PAGESZ
; - 当申请的内存大小小于等于
XNHEAP_MINALIGNSZ
16Bytes时,按照字节对齐的方式分配内存空间,size = ALIGN(size, XNHEAP_MINALLOCSZ);
; @XNHEAP_MINALLOCSZ = 8Bytes - 当申请的内存大小介于中间
XNHEAP_MINALIGNSZ < size < XNHEAP_PAGESZ
时,按照16字节对齐的方式分配内存空间,size = ALIGN(size, XNHEAP_MINALIGNSZ);
; @XNHEAP_MINALIGNSZ = 16Bytes
- 当申请的内存大小大于一页
if (likely(size <= XNHEAP_PAGESZ * 2))
// 如果申请的内存大小小于 <2Pages 大小时,使用 bucketed memory blocks* 进行内存分配bsize = size < XNHEAP_MINALLOCSZ ? XNHEAP_MINALLOCSZ : size;
// 将 size 的值设置为大于XNHEAP_MINALLOCSZ
大小的值log2size = order_base_2(bsize);
// order_base_2 函数用来寻找第一个大于输入参数 bsize 的2的幂次的次数值,存储在 log2size 返回值当中bsize = 1 << log2size;
// 将需要申请的内存空间 size 大小转换为2的整幂次的值 bsizeilog = log2size - XNHEAP_MINLOG2;
// ilog 变量中存储的是 申请空间的总字节数/8Bytes的结果,表示为 8Bytes 的整倍数xnlock_get_irqsave(&heap->lock, s);
// 在操作全局系统的 heap 对象前,应该获取锁避免 heap 内存操作竞争block = heap->buckets[ilog].freelist;
// 取出 buckets 管理的 8Bytes 大小的内存池空闲列表,ilog 相当于一个标志位,标志这 log2size - XNHEAP_MINLOG2 大小的内存块.if (block == NULL)
// 判断是否还未分配产生过第一个 buckets 内存块,如果未产生则寻找满足要求的全新的一页中开始分配,如下block = get_free_range(heap, bsize, log2size);
// 如果取出的 freelist 为空地址,则调用此接口获取大小为 \(2^{log2size}\) 大小的连续空闲页,调用此接口之前必须要获取到 heap-lock 才能操作,通过不断的向后延伸直到取得满足申请 heap bsize 大小的条件则记录下当前的lastpage
内存结束页地址,内存分配起始地址headpage
,并更新当前Xenomai系统的空闲页起始地址heap->freelist = *((caddr_t *)lastpage);
,这里需要注意函数中使用的do{...}while(...)
语句中的操作,实际上完成了对heap->freelist
的扫描: 参考这里:init_freelistdo {
lastpage = freepage;
freepage = *((caddr_t *) freepage); // 这里取地址中的值作为下一页的起始地址,参见测试代码.
freecont += XNHEAP_PAGESZ;
}
while (freepage == lastpage + XNHEAP_PAGESZ && freecont < bsize);
取强转后的地址中的值作为下一页的起始地址,而地址中的值在
init_freelist
函数中已经完成了赋值操作,关于地址强转取内容的测试如下 PointerCastTest:typedef void * vaddr_t; vaddr_t p;
p = (vaddr_t)malloc(64);
*(int *)p = 10;
for(int i=0; i<64 ; i++)
{
*(int *)(p+i) = 10;
// printf("The Data Content of P[%d]:%x\n", i, *(int *)(p+i));
}
vaddr_t PTest = p;
printf("PTest = %p=%p @ PTest Address:%p Content:%x\n", PTest, p, &PTest, *(int *)PTest);
PTest = *((vaddr_t*)PTest);
printf("PTest = %p=%p @ PTest Address:%p\n", PTest, p, &PTest);
测试结果如下 (AM5718@ARM 32Bits SMP Arch) :
root@MM5718v1:~# ./Burnish/RTDemoExe
PTest = 0x2b0f8=0x2b0f8 @ PTest Address:0xbef10af0 Content:a0a0a0a
PTest = 0xa0a0a0a=0x2b0f8 @ PTest Address:0xbef10af0
- goto splitpage; 完成空闲页的申请操作后,跳转到页分块的位置进行分块,进入到这个位置后的 headpage 是第一个满足大于等于需求内存大小的连续内存空间的起始页地址.
- 当申请的内存空间大小 bsize < XNHEAP_PAGESZ 小于页大小时,将该页剩余的内存空间切割为和 bsize 大小相同的若干个 heap block 内存块,同时使用 block 作为块内存的 list 记录下来. 如果申请空间大于等于一页的时候,将 *((caddr_t *)headpage) 设置为
NULL
,实际即为将 headpage 指向的下一页地址清除,仅保留当前页的起始地址. pagenum = (headpage - heap->membase) / XNHEAP_PAGESZ;
// 计算当前页的相对于内存 heap->membase 起始地址的间隔总数并除以 pagesize 从而计算出 pagenumheap->pagemap[pagenum].type = log2size ? : XNHEAP_PLIST;
// 根据输入的 log2size 参数,确定是否为块的开始,或者确切的子块的大小 log2size,页按照 size 大小分割许多子块.heap->pagemap[pagenum].bcount = 1;
// 设置当前块为激活状态for (pagecont = bsize / XNHEAP_PAGESZ; pagecont > 1; pagecont--)
// 当申请的空间大小大于 2*XNHEAP_PAGESZ 的情况下时,配置每一页为 XNHEAP_PCONT 类型,块激活状态为 0.
- 当申请的内存空间大小 bsize < XNHEAP_PAGESZ 小于页大小时,将该页剩余的内存空间切割为和 bsize 大小相同的若干个 heap block 内存块,同时使用 block 作为块内存的 list 记录下来. 如果申请空间大于等于一页的时候,将 *((caddr_t *)headpage) 设置为
- goto splitpage; 完成空闲页的申请操作后,跳转到页分块的位置进行分块,进入到这个位置后的 headpage 是第一个满足大于等于需求内存大小的连续内存空间的起始页地址.
else
// 如果已经存在heap->buckets[ilog].freelist
则直接从中分配,并将 ilog 大小的块--heap->buckets[ilog].fcount;
的总数减一.pagenum = ((caddr_t)block - heap->membase) / XNHEAP_PAGESZ;
// 页编号通过 block 与 membase 起始地址间隔,从而计算出页的编号++heap->pagemap[pagenum].bcount;
// 根据计算的到的页的编号,将对应激活块的数量+1.
heap->buckets[ilog].freelist = *((caddr_t *)block);
// 完成块的操作之后,更新buckets[ilog].freelist
的列表,向后延申heap->used += bsize;
// 记录系统 heap 总使用量.
else {
// 针对分配内存大于 >2Pages 的处理逻辑block = get_free_range(heap, size, 0);
// 直接使用该函数分配足够的页即可heap->used += size;
// 分配成功则统计 heap 总使用量.
if (heapobj_init(&hcb->hobj, NULL, heapsz))
// 使用 TLSF 内存管理架构申请大片内存空间,申请失败则返回 [-ENOMEM]
__heapobj_init_private(hobj, name, size, NULL);
// 系统调用,将通过 TLSF 算法申请到的 heap 内存空间赋值给 hobj 结构体管理,可以看到 tlsf 算法实际申请的内存空间的位置在 cobalt 管理的 private-mem-pool 区域.size += tlsf_pool_overhead;
// 这里使用全局变量记录了 字节数量的起始值 tlsf_pool_overhead
测试调试方法
cobalt 内核代码部分调试:
printk(XENO_WARNING "disabled on kernel command line\n");
// 使用 printk 来进行调试输出打印
cobalt 系统调用接口调试:
warning("[%s]%d\n",__FUNCTION__,__LINE__);
// 系统调用就是用系统打印进行输出printf("[%s]%d\n",__FUNCTION__,__LINE__);
// 系统调用就是用系统打印进行输出
Xenomai 源码分析-Part I的更多相关文章
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- nginx源码分析之网络初始化
nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- zookeeper源码分析之三客户端发送请求流程
znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...
- java使用websocket,并且获取HttpSession,源码分析
转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...
- ABP源码分析二:ABP中配置的注册和初始化
一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...
- ABP源码分析三:ABP Module
Abp是一种基于模块化设计的思想构建的.开发人员可以将自定义的功能以模块(module)的形式集成到ABP中.具体的功能都可以设计成一个单独的Module.Abp底层框架提供便捷的方法集成每个Modu ...
- ABP源码分析四:Configuration
核心模块的配置 Configuration是ABP中设计比较巧妙的地方.其通过AbpStartupConfiguration,Castle的依赖注入,Dictionary对象和扩展方法很巧妙的实现了配 ...
随机推荐
- oracle中查询表字段信息及主键字段
select a.owner, a.table_name, a.column_name, a.data_type, d.constraint_type, a.num_nulls from all_ta ...
- 在Scorpio 1.0(天蝎座)中使用C# System.Diagnostics.Process打开chrome遇到的问题
1 //在天蝎座 中使用C# System.Diagnostics.Process打开chrome遇到的问题 2 DiagProcess = import_type("System.Diag ...
- 理解 Shell
理解 Shell shell 的父子关系 用于登录的某个虚拟控制器终端,或在 GUI 中运行终端仿真器时所启动的默认的交互 shell,是一个父 shell.本书到目前为止都是父 shell 提供 C ...
- 解决Z490-A吹雪安装macOS Monterey随机重启
1.目前发现随机重启问题是板载网卡I225-v导致,需要去除以往的网卡的kext补丁: 2.去除补丁后发现网络连接识别成功,但是无法上网:这是因为网卡ID注入错误.需要将网卡ID设置为:F315868 ...
- 字典方法 setdefault()、pprint;迭代、递归的区别
计算一个字符串中每个字符出现的次数 import pprint message = 'It was a bright cold day in April, ' \ 'and the clocks we ...
- cesium 學習計劃
上篇已经将cesium环境搭建完成,并且服务启动完成,进去后主要浏览 documents 文档和Sandcastle 示例. 学习计划:沙盒示例学习一遍,每个示例中的查看对应代码接口. 学习cesiu ...
- jmeter性能测试小小实践
一.测试步骤 测试计划 / 线程组 / http请求 / 监听器 / 运行脚本 / 查看报告 二.线程组 线程组:虚拟用户数 ramp up period:设置虚拟用户数需要多长的时间全部启动,如果线 ...
- Unity 转小游戏
填写appid 和游戏资源位置 在导出的项目里可以修改游戏资源位置 两个目录 minigame 是小程序打开的目录 webgl 是要下载的的资源 下载一个http 服务器就有了 和JS交互 大部分js ...
- pip安装报错 cannot uninstall a distutils installed project
sudo pip install --ignore-installed xxx 在安装jupyter notebook的时候,遇到了这个问题,于是上网搜索,搜到了靠谱答案github解决方案 sudo ...
- swiper常见问题、动态加载数据问题
swiper加载静态文件是没有问题的 swiper加载动态文件需要在请求后再加载这个函数 参考链接: https://blog.csdn.net/webzrh/article/details/781 ...