源码解读·RT-Thread操作系统从开机到关机
本篇内容比较简单,但却很繁琐,篇幅也很长,毕竟是囊括了整个操作系统的生命周期。这篇文章的目的是作为后续设计多任务开发的铺垫,后续会单独再抽出一篇分析任务的相关知识。另外本篇文章以单核MCU为背景,并且以最新的3.1.xLTS版本源码进行分析。主要内容目录如下:
基于bsp/stm32/stm32f103-mini-system为背景
Cortex-M3的堆栈基础概念
C语言main函数和rt-thread的main
rt-thread操作系统的传统初始化与自动初始化组件
任务是怎样运行起来的
Idle任务与新的构想
基于bsp/stm32/stm32f103-mini-system的开机介绍
关于体系结构的知识这里不做过多的介绍,因为这些知识要讲清楚的话足以写出一本大部头的书出来。不过会简单介绍一些必要的东西。
Stm32f103单片机是cortex-m3内核,在cortex-m3内核中使用双堆栈psp和msp,模式分为线程模式和handler模式,权限级别分为非特权级别和特权级别(现在只需要知道这么多就行了),handler模式就是当处理发生中断的时候自动进入的模式,其handler模式永远为特权级。
上电开机最开始运行的是MCU内部的ROM部分,这部分代码我们通常看不到,其通常是对芯片进行必要的初始化,比如FLASH和RAM的时钟初始化等,然后跳转到用户flash区域运行用户代码。在STM32中用户flash地址从0x08000000开始。我们写的代码都是从这里开始运行的。其次由于cortexM规定其用户FLASH区域的最前面必须是一张中断向量表。所以也就是说STM32的0x08000000开始是一张中断向量表,这是必须的也是默认的,当然在之后还可以重映射其它地方的向量表。这张向量表中的第一项是一个栈地址,第二项复位向量地址。下面贴一段向量表部分代码(摘录自startup_stm32f103xb.s):
__Vectors DCD __initial_sp ; Topof Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMIHandler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD ; Reserved
DCD ; Reserved
DCD ; Reserved
DCD ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
另外需要注意的是开机后会自动进入复位异常,通常我们叫上电复位过程,不过意外的是上电复位处理的模式是特权级线程模式。在特权模式下堆栈指针将使用MSP,非特权模式下可以被切换到PSP。RT-Thread操作系统就是这么做的。所以回过头来看,中断向量表第一项指定了MSP的栈起始地址,并被自动加载到MSP,第二项指定了复位向量地址,也被自动加载到PC并运行。这样一来开机后我们能通过debug看到PC指针最先指向复位向量的第一条指令上。我们看一下stm32f103在armcc编译器上的复位向量代码:
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
这是一段汇编代码,其完成两件事,第一件事调用systemInit函数完成一些初始化,第二件事跳转到__main函数。其中systemInit函数我们是可以找到并可以修改的一个C语言实现的函数(暂时不讨论,有兴趣的可以看system_stm32f1xx.c)。而这个__main就牛逼了,这既不是我们自己写的C语言的main也看不到它在哪里实现的。但是现在进入__main后它就是会跑到你最终用C语言写的main。这个__main的来龙去脉稍后会在第三部分分析。
Cortex-M3的堆栈基础概念
在Cortex-M3的处理器内核上堆栈指针分为PSP和MSP。handler模式下总是使用MSP,线程模式可以通过CONTROL寄存器来配置(修改的时候必须处于特权模式才可以)。
之所以需要这样设计就是为了将普通软件和系统软件通过权限隔离开,避免普通用户权限操作系统关键资源带来安全风险。当我们使用带有操作系统的环境进行开发时,操作系统就会将关键操作例如任务切换、中断处理等在特权模式操作。而其它的操作都会运行在非特权模式下完成。
操作系统一般都会将必要的操作封装出API接口,以提供给普通软件调用。而这背后的设计思想就是通过触发异常,然后进入特权模式运行异常向量处理程序。而这段异常处理程序早就让操作系统实现了,进而这部分特权操作是操作系统接管处理的。这也就避免用户普通软件去进行不必要的特权操作。例如用户任务想主动放弃CPU从而调用yield,yield将进行任务切换,其中过程大概是“选出另一个任务”->”触发SVC或者Pendsv异常”->进入SVC/Pendsv的handler异常处理程序,此时是特权模式,完成操作后返回到新任务运行。在RT-Thread中进入任务切换是通过触发Pendsv异常。
C语言main函数和RT-Thread的main
前面提到过开机启动最后进入复位向量处运行,最终调用__main就跑到我们外面写的C语言的main函数了。但这并非这么简单,在从__main到我们的main中间还有一系列操作比如初始化堆栈、初始化全局变量区域、初始化C运行时库等,然后再在最后调用用户的main函数。
不过在不同的编译器上这个__main并非是固定的,这里也就armcc是如此,如果是GCC和IAR的话其就不太一样,不过不影响我们分析核心主题。这里仅以借用armcc为例来分析主题中心思想。另外在说明RT-Thread中开启RT_USING_USER_MAIN的时候在ARMCC编译器上还有一个支持挂钩的操作,这种操作一般见于补丁修复的时候。其实现方式是在原有函数的名字前加上$Sub$$
前缀就可以将原有函数劫持下来,并通过加上$Super$$
前缀再调用原始函数。具体如下:The followingexample shows how to use $Super$$and $Sub$$ to insert a callto the function ExtraFunc() before the call to the legacy functionfoo().
extern void ExtraFunc(void);
extern void $Super$$foo(void); /* this functionis called instead of the original foo() */
void $Sub$$foo(void)
{
ExtraFunc(); /* does some extra setup work */
$Super$$foo(); /* calls the original foo() function */
/* To avoid calling the original foo() function
* omit the $Super$$foo(); function call.
*/
}
上例中原本有一个原始函数叫做foo,但是现在通过$Sub$$foo来劫持所有调用foo的地方,自动会调用$Sub$$foo,然后新的$Sub$$foo里面先调用自己的扩展实现ExtraFunc后,再接着调用原始版本的foo函数,不过调用原始的foo是加了前缀$Super$$的$Super$$foo.
当使用RT-Thread操作系统开启RT_USING_USER_MAIN后就是利用这种骚操作来完成RT-Thread操作系统的初始化过程的。(代码摘录自components.c)
extern int $Super$$main(void);
/* re-definemain function */
int $Sub$$main(void)
{
rtthread_startup();
return ;
}
关于rtthread_startup函数稍后再讲解,不过先接着看下面这个函数:
/* the systemmain thread */
void main_thread_entry(void*parameter)
{
extern int main(void);
extern int $Super$$main(void); /* RT-Thread components initialization*/
rt_components_init(); /* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
上面这个函数其实是个小任务,就是完成组件初始化后再跳转到用户main函数的。这个小任务在rtthread_startup中调用rt_application_init时创建的,所以此时rt-thread系统早就以经跑起来了。也就是说当调用rtthread_startup后正常情况就不再会返回到原来的调用地方,接下来会交给系统的调度器去接管,切换运行任务去了。看下面的代码了解rt_application_init:
void rt_application_init(void)
{
rt_thread_t tid; #ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, );
RT_ASSERT(tid != RT_NULL);
#else
rt_err_t result; tid = &main_thread;
result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, );
RT_ASSERT(result == RT_EOK); /* if not define RT_USING_HEAP, using toeliminate the warning */
(void)result;
#endif rt_thread_startup(tid);
}
至此,关于各种main的子子孙孙以经差不多了解清楚了,其流程大概如下:
ResetHandle->__main->$Sub$$main->(rtthread_startup->rt_application_init->main_thread_entry)->$Super$$main。
其中$Super$$main就是我们的用户main函数。如果没有启用RT_USING_USER_MAIN那就简单了,其流程如下:
ResetHandle->__main->main
接下来再接着分析$Sub$$main中调用的rtthread_startup函数。
RT-Thread操作系统的传统初始化与自动初始化组件
这里着重讨论rtthread_startup函数,因为这就是RT-Thread操作系统的入口和初始化流程。不过既然说到rtthread_startup函数了,就不得不一起介绍一下RT-Thread操作系统的自动初始化组件了。
rtthread_startup函数是一个函数调用链,依次调用各个阶段的初始化函数,并在最后启动调度器不再返回。代码摘录自components.c
int rtthread_startup(void)
{
rt_hw_interrupt_disable(); /* board level initialization
* NOTE: please initialize heap insideboard initialization.
*/
rt_hw_board_init(); /* show RT-Thread version */
rt_show_version(); /* timer system initialization */
rt_system_timer_init(); /* scheduler system initialization */
rt_system_scheduler_init(); #ifdef RT_USING_SIGNALS
/* signal system initialization */
rt_system_signal_init();
#endif /* create init_thread */
rt_application_init(); /* timer thread initialization */
rt_system_timer_thread_init(); /* idle thread initialization */
rt_thread_idle_init(); /* start scheduler */
rt_system_scheduler_start(); /* never reach here */
return ;
}
以上代码我们主要脉络是这样的:先关闭全局中断->初始化硬件板上的资源->打印RT-Thread的LOGO->系统定时器功能初始化->调度器初始化->signal功能初始化->应用程序初始化(这个通常是用来创建用户任务的)->系统软timer任务初始化->系统idle任务初始化->启动调度器,永远不再返回。
这里我们先来说一下为什么要先关闭全局中断,因为在初始化过程中,有可能MCU就有其它的中断和异常触发了,这个时候系统还没有初始化完成,这就势必导致系统出现故障,所以先关闭全局中断,并在启动调度器后再打开。
rt_hw_board_init非常关键,在这个函数里面必须完成一些必须的初始化过程:堆内存系统的初始化和硬件资源模块以及如果开启了自动初始化组件时还需要调用rt_components_board_init完成必要的初始化,这个函数是自动初始化组件的一个接口。(代码摘录自bsp\stm32\libraries\HAL_Drivers\drv_common.c)
RT_WEAK void rt_hw_board_init()
{
#ifdef SCB_EnableICache
/* EnableI-Cache---------------------------------------------------------*/
SCB_EnableICache();
#endif #ifdef SCB_EnableDCache
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
#endif /* HAL_Init() function is called at thebeginning of the program */
HAL_Init(); /* System clock initialization */
SystemClock_Config();
rt_hw_systick_init(); /* Heap initialization */
#if defined(RT_USING_HEAP)
rt_system_heap_init((void*)HEAP_BEGIN, (void*)HEAP_END);
#endif /* Pin driver initialization is open bydefault */
#ifdef RT_USING_PIN
rt_hw_pin_init();
#endif /* USART driver initialization is openby default */
#ifdef RT_USING_SERIAL
rt_hw_usart_init();
#endif /* Set the shell console output device*/
#ifdef RT_USING_CONSOLE
rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif /* Board underlying hardwareinitialization */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
}
然后回到rtthread_startup函数中再看rt_application_init函数,由于我们是用的stm32的BSP,这个bsp系列是使用自动初始化组件和RT_USING_USER_MAIN功能的,所以过程稍微隐蔽一些,先是在rt_application_init中创建了一个小任务,然后再在小任务中调用了rt_components_init,这也是自动初始化组件的接口。如果没有开启自动初始化组件的话,通常我们的用户任务可以在rt_application_init中创建了。也可以像这里的实现一样,先创建一个小任务,然后再在小任务里完成一些初始化和创建用户任务。
然后再回到rthtread_startup中看到有初始化软timer和idle任务的,其中软件timer功能是可以通过裁剪配置选择的,如果打开后就可以在后续创建softtimer。否则所有的timer都会在OS TICK的中断上下文中计时。另外这个idle任务也是系统中必不可少和优先级最低的任务。即使我们启动调度器后没有创建任何用户任务,系统中也有一个idle任务在运行。Idle任务的优先级最低,在此我建议开发人员最好不要将自己的用户任务优先级配置成最低以免和idle竞争时间片,这会给你今后的开发带来不必要的麻烦。关于这个问题,我最后会提出一些新的设计构想。不过这里先要介绍一下idle任务的功能。Idle任务会在系统空闲时被调度运行,所以我们通常在idle任务里做低功耗设计。其次idle任务里还会完成系统资源的回收。例如被删除的任务,被删除的module等。
最后rthtread_startup启动调度器rt_system_scheduler_start开始调度系统的任务,从此就开始运行任务,不再返回。这里又要记住一个概念,在上文提到的PSP和MSP,到目前为止MCU还是使用一开始中断向量表中指定的MSP栈。但是当调度任务后,任务会有自己的栈,且rt-thread系统会将任务的栈切换到PSP栈指针。值得注意的是,这个MSP是全局共享的,所有的中断程序都会使用这个栈空间,所以我们需要根据自己的情况来配置这个MSP栈的空间大小。
接下来我们再来介绍自动初始化组件。RT-Thread中的自动初始化组件思路来自于Linux内核。其实现手段是将需要初始化的函数接口通过链接器指令放在特殊的section中。这个section的概念是当我们程序最终链接成一个image后会形成一个标准格式的文件,其中armcc中叫做ARM ELF。详细的介绍可以查阅官方资料。其中ELF文件就有将代码分成称为section的区域,可以称作段。并且可以指定自己的代码放在指定名称的段中,且可以指定这个section段的ROM地址。这样当我们设计玩初始化接口后,通过链接器的指令以及链接脚本文件将我们的初始化代码放在特定的地方,并且利用命名规则来做到顺序排序。等需要调用初始化的时候可以利用这些section的地址转换成函数指针直接批量循环调用。通常你会在MDK的工程文件链接器参数中看到这样的指令:--keep *.o(.rti_fn.*),这是为了在链接阶段保证这些自定义段不被删除。同时也可以看出rti_fn就是自动初始化组件的section名字。类似的将函数放置在这些段中的链接器指令如下:(摘录自rtdef.h)
/*initialization export */
#ifdef RT_USING_COMPONENTS_INIT
typedef int (*init_fn_t)(void);
#ifdef _MSC_VER/* we do notsupport MS VC++ compiler */
#define INIT_EXPORT(fn,level)
#else
#if RT_DEBUG_INIT
struct rt_init_desc
{
const char* fn_name;
const init_fn_t fn;
};
#define INIT_EXPORT(fn, level) \
const char __rti_##fn##_name[] =#fn; \
RT_USED const struct rt_init_desc __rt_init_desc_##fn SECTION(".rti_fn."level)=\
{ __rti_##fn##_name, fn};
#else
#define INIT_EXPORT(fn, level) \
RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn."level)= fn
#endif
#endif
#else
#define INIT_EXPORT(fn, level)
#endif /* board initroutines will be called in board_init() function */
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn,"1") /*pre/device/component/env/app init routines will be called in init_thread */
/* componentspre-initialization (pure software initilization) */
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn,"2")
/* deviceinitialization */
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn,"3")
/* componentsinitialization (dfs, lwip, ...) */
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn,"4")
/* environmentinitialization (mount disk, ...) */
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn,"5")
/* appliationinitialization (rtgui application etc ...) */
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn,"6")
其中不同的数字代表不同的初始化顺序,可以根据需要来选择。接着如上文提到的两个函数rt_components_board_init和rt_components_init是如何实现的:摘录自components.c
#ifdef RT_USING_COMPONENTS_INIT
/*
* Components Initialization will initializesome driver and components as following
* order:
* rti_start --> 0
* BOARD_EXPORT --> 1
* rti_board_end --> 1.end
*
* DEVICE_EXPORT --> 2
* COMPONENT_EXPORT --> 3
* FS_EXPORT --> 4
* ENV_EXPORT --> 5
* APP_EXPORT --> 6
*
* rti_end --> 6.end
*
* These automatically initialization, thedriver or component initial function must
* be defined with:
* INIT_BOARD_EXPORT(fn);
* INIT_DEVICE_EXPORT(fn);
* ...
* INIT_APP_EXPORT(fn);
* etc.
*/
static int rti_start(void)
{
return ;
}
INIT_EXPORT(rti_start,""); static int rti_board_start(void)
{
return ;
}
INIT_EXPORT(rti_board_start,"0.end"); static int rti_board_end(void)
{
return ;
}
INIT_EXPORT(rti_board_end,"1.end"); static int rti_end(void)
{
return ;
}
INIT_EXPORT(rti_end,"6.end"); /**
* RT-Thread Components Initialization forboard
*/
void rt_components_board_init(void)
{
#if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
#else
const init_fn_t *fn_ptr; for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
#endif
} /**
* RT-Thread Components Initialization
*/
void rt_components_init(void)
{
#if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc; rt_kprintf("do components initialization.\n");
for (desc = &__rt_init_desc_rti_board_end; desc < &__rt_init_desc_rti_end; desc++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
#else
const init_fn_t *fn_ptr; for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr++)
{
(*fn_ptr)();
}
#endif
}
之所以要分开这两个函数就是因为board阶段的初始化比其它普通的组件初始化早,board阶段的初始化通常没什么系统资源依赖。而其它情况下则通常在操作系统已经完成必要的初始化后才能做的初始化才会放在rt_components_init里。
任务是怎样运行起来的
要说明任务是怎么运行起来的,就得知道任务是怎么创建的,其次结合之前写的文章<源码解读·RT-Thread多任务调度算法>就差不多了。那么这里就介绍一下任务的创建。照样用上面的rt_application_init里创建任务的代码来举例:
void rt_application_init(void)
{
rt_thread_t tid; #ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, );
RT_ASSERT(tid != RT_NULL);
#else
rt_err_t result; tid = &main_thread;
result =rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
main_stack, sizeof(main_stack),RT_MAIN_THREAD_PRIORITY, );
RT_ASSERT(result == RT_EOK); /* if not define RT_USING_HEAP, using toeliminate the warning */
(void)result;
#endif rt_thread_startup(tid);
}
首先要说明的是RT-Thread任务创建有两种,一种是动态的,一种是静态的。所谓的动态就是其任务栈自动在堆内存中分配;静态是用户自己指定栈空间,当然通常这个栈来自于用户定义的数组。如上例中当RT_USING_HEAP宏被打开,也就是有堆内存的时候会采用rt_thread_create接口来创建动态资源的任务。当然可以利用rt_thread_init来创建一个静态资源的任务。先来了解一下这两个函数在创建任务时的一些参数:”main”这是任务的名称,任务名称用一个字符串来指定,不是很重要,不过最好能起到一定的说明性,有利于今后调试用。main_thread_entry这是任务的入口函数,所谓的任务就是一个C语言中的函数而已。RT_NULL,这是传给任务入口函数的参数,如果没有就为NULL.因为RT_Thread中的任务原型为:void (*entry)(void*parameter);RT_MAIN_THREAD_STACK_SIZE为任务的栈大小,以字节为单位。RT_MAIN_THREAD_PRIORITY为任务的优先级号。20为任务的时间片大小。其中静态任务中还有tid代表任务的TCB数据结构句柄。main_stack为栈空间起始地址。当用动态创建的方法创建成功后会返回一个任务的TCB任务句柄出来。之后我们利用rt_thread_startup(任务句柄)的形式启动任务即可。例如上例中rt_thread_startup(tid);不过rt_thread_startup函数真正的功能是将任务放置于调度队列中,并置任务状态为ready,由此交给调度器去调度,能不能立马运行取决与调度器的调度。一般情况下,要想任务获得运行必须满足的条件:调度器已经运行,任务已经ready,没有更高优先级任务,没有中断发生。只要条件满足调度器就会调度此任务,做好必要的栈初始化和状态置位,就会切换到任务开始运行。只要任务获得运行就会使用创建任务时指定的栈空间。
不过一般的任务通常是一直运行,持续的服务。形式如下:
void task(void *parameter)
{
while ()
{
// do_work();
}
}
idle任务与新的构想
上面解释过idle任务在rt-thread操作系统中的功能:释放资源、低功耗设计。
关于资源释放通常是任务的析构过程,这就是任务的结束。例如上例中的main_thread_entry任务之所以称为小任务的原因就是它做完事情就结束了。那么可能就会想,既然任务都结束了那么它的资源如何释放呢?比如栈空间,TCB等。这就是idle该干的事情。即使所有的用户任务都结束,最后也会剩下idle任务在运行。如果有必要的话,可以在idle任务中可以通过调用低功耗组件进入低功耗或者干脆调用电源开关控制来关机。
其次idle任务占用了最低优先级。虽然用户任务也可以使用和idle任务相同的优先级,但是并不建议这样做,比如在低功耗设计时就会出问题。另外我个人在思考一个问题,idel任务既然以经在设计之初就明确了其获得运行的条件,那么何不做成无需优先级的任务,唯一的调度决策就是:当调度器没有任务处于ready状态时就切换到idel任务运行。这就无需关注最低优先级被idle霸占的问题了。
感谢各位网友的支持,可以关注我的微信公众号:鹏城码夫 (微信号:rocotona)
源码解读·RT-Thread操作系统从开机到关机的更多相关文章
- java.lang.system 类源码解读
通过每块代码进行源码解读,并发现源码使用的技术栈,扩展视野. registerNatives 方法解读 /* register the natives via the static initializ ...
- 线程本地变量ThreadLocal源码解读
一.ThreadLocal基础知识 原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板 ...
- [Hadoop源码解读](六)MapReduce篇之MapTask类
MapTask类继承于Task类,它最主要的方法就是run(),用来执行这个Map任务. run()首先设置一个TaskReporter并启动,然后调用JobConf的getUseNewAPI()判断 ...
- 【原】Spark中Job的提交源码解读
版权声明:本文为原创文章,未经允许不得转载. Spark程序程序job的运行是通过actions算子触发的,每一个action算子其实是一个runJob方法的运行,详见文章 SparkContex源码 ...
- Normalize.css 介绍与源码解读
开始 Normalize.css 是一个可定制的 CSS 文件,使浏览器呈现的所有元素,更一致和符合现代标准;是在现代浏览器环境下对于CSS reset的替代. 它正是针对只需要统一的元素样式.该项目 ...
- SDWebImage源码解读之SDWebImageManager
第九篇 前言 SDWebImageManager是SDWebImage中最核心的类了,但是源代码确是非常简单的.之所以能做到这一点,都归功于功能的良好分类. 有了SDWebImageManager这个 ...
- HttpClient 4.3连接池参数配置及源码解读
目前所在公司使用HttpClient 4.3.3版本发送Rest请求,调用接口.最近出现了调用查询接口服务慢的生产问题,在排查整个调用链可能存在的问题时(从客户端发起Http请求->ESB-&g ...
- Alamofire源码解读系列(六)之Task代理(TaskDelegate)
本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...
- AbstractQueuedSynchronizer源码解读
1. 背景 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)是Doug Lea大师创作的用来构建锁或者其他同步组件(信号量.事件等) ...
随机推荐
- Python抓取小说
Python抓取小说 前言 这个脚本命令MAC在抓取小说写,使用Python它有几个码. 代码 # coding=utf-8 import re import urllib2 import chard ...
- aspnetboilerplate && .net core 使用原生sql
利用aspnetboilerplate提供的工具类IDbContextProvider private readonly IDbContextProvider<XXXDbContext> ...
- 使用StringBuilder与SqlParameter
好处: 防止sql注入:占用内存更少 例子: 传参有业务查询条件startDate,endDate,A,每页数据个数pageSize,当前查询页码pageIndex string sql = @&qu ...
- js错误界面
<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...
- 运行control userpasswords2实现winXP自动登录
原文:运行control userpasswords2实现winXP自动登录 如果你的计算机只是自己一人在用,且每次都用同一个用户名(或者你根本没在意过什么是用户名),而每次都要输入密码,是否太麻烦了 ...
- WPF媒体资源和图片资源寻址方式的杂谈
WPF提供一个封装和存取资源(resource)的机制,我们可将资源建立在应用程序的不同范围上.WPF中,资源定义的位置决定了该资源的可用范围.资源可以定义在如下范围中: (1)控件级:此时,资源只能 ...
- vs2017 cordova调试android app
方案是:virtualbox + androidx86 7.0+_x64.iso虚拟机方式
- C++ GUID和string转化函数【转载】
原文地址:https://blog.csdn.net/zgl7903/article/details/5488294 因为这两个函数太好用,解决了大问题,我必须转载一下了.转自csdn牛人 zgl79 ...
- 领域驱动设计(DDD)的实践经验分享之持久化透明
原文:领域驱动设计(DDD)的实践经验分享之持久化透明 前一篇文章中,我谈到了领域驱动设计中,关于ORM工具该如何使用的问题.谈了很多我心里的想法,大家也对我的观点做了一些回复,或多或少让我深深感觉到 ...
- LINUX基础内容
在Linux中,有三种基本的文件类型: 1) 普通文件 普通文件是以字节为单位的数据流,包括文本文件.源码文件.可执行文件等.文本和二进制对Linux来说并无区别,对普通文件的解释由处理该文件的应用程 ...