Linux进程管理 (篇外)内核线程简要介绍【转】

时间:2023-01-05 23:12:39

转自:https://www.cnblogs.com/arnoldlu/p/8336998.html

关键词:kthread、irq、ksoftirqd、kworker、workqueues

在使用ps查看线程的时候,会有不少[...]名称的线程,这些有别于其它线程,都是内核线程。

其中多数内核线程从名称看,就知道其主要功能。

比如给中断线程化使用的irq内核线程,软中断使用的内核线程ksoftirqd,以及work使用的kworker内核线程。

本文首先概览一下Linux都有哪些内核线程,然后分析创建内核线程的API。

在介绍内核线程和普通线程都有哪些区别?

最后介绍主要内核线程(irq/ksoftirqd/kworker/)的创建过程及其作用。

1. ps下初步认识Linux内核线程

在ps -a会显示如下,可以看出内核线程都用[...]标注。

并且pid=1的init进程是所有用户空间进程的父进程;pid=2的kthreadd内核线程是所有内核线程的父线程。

内核线程分为几大类:softirq、kworker、irq及其他。

Linux进程管理 (篇外)内核线程简要介绍【转】
PID   USER     TIME   COMMAND
1 0 0:01 {linuxrc} init
2 0 0:00 [kthreadd]
3 0 0:00 [ksoftirqd/0]
4 0 0:00 [kworker/0:0]
5 0 0:00 [kworker/0:0H]
6 0 0:00 [kworker/u8:0]
7 0 0:00 [rcu_sched]
8 0 0:00 [rcu_bh]
9 0 0:00 [migration/0]
10 0 0:00 [migration/1]
11 0 0:00 [ksoftirqd/1]
12 0 0:00 [kworker/1:0]
13 0 0:00 [kworker/1:0H]
14 0 0:00 [migration/2]
15 0 0:00 [ksoftirqd/2]
16 0 0:00 [kworker/2:0]
17 0 0:00 [kworker/2:0H]
18 0 0:00 [migration/3]
19 0 0:00 [ksoftirqd/3]
20 0 0:00 [kworker/3:0]
21 0 0:00 [kworker/3:0H]
22 0 0:00 [khelper]
23 0 0:00 [kdevtmpfs]
24 0 0:00 [perf]
25 0 0:00 [kworker/u8:1]
279 0 0:00 [khungtaskd]
280 0 0:00 [writeback]
281 0 0:00 [kintegrityd]
282 0 0:00 [kworker/0:1]
284 0 0:00 [bioset]
286 0 0:00 [kblockd]
294 0 0:00 [ata_sff]
408 0 0:00 [rpciod]
409 0 0:00 [kworker/2:1]
410 0 0:00 [kworker/1:1]
412 0 0:00 [kswapd0]
416 0 0:00 [fsnotify_mark]
429 0 0:00 [nfsiod]
449 0 0:00 [kworker/3:1]
527 0 0:00 [kpsmoused]
537 0 0:00 [kworker/1:2]
613 0 0:00 [deferwq]
Linux进程管理 (篇外)内核线程简要介绍【转】

2. kthreadd以及创建内核线程API

2.1 kthreadd:kthreadd内核线程的创建

内核其他线程的创立,要基于kthreadd。kthreadd线程是其他线程的父线程。

start_kernel-->rest_init如下:

Linux进程管理 (篇外)内核线程简要介绍【转】
static noinline void __init_refok rest_init(void)
{
int pid; rcu_scheduler_starting();
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
kernel_thread(kernel_init, NULL, CLONE_FS);--------------------------------创建第一个用户空间线程init
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);---------------创建第一个内核线程kthreadd
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);--------------------kthreadd_task指向kthreadd的task_strcut结构体
rcu_read_unlock();
complete(&kthreadd_done);--------------------------------------------------在init进程kernel_init-->kernel_init_freeable中等待kthreadd_done释放 /*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
init_idle_bootup_task(current);
schedule_preempt_disabled();
/* Call into cpu_idle with preempt disabled */
cpu_startup_entry(CPUHP_ONLINE);
}
Linux进程管理 (篇外)内核线程简要介绍【转】

kernel_init在kthreadd之前启动,但是kernel_init的很多任务需要基于kthreadd。所以在kernel_init的开头等待reset_init的kthreadd_done完成量。

因为kernel_init-->kernel_init_freeable-->do_basic_setup-->do_initcalls中很多初始化需要kthread_create支援。

Linux进程管理 (篇外)内核线程简要介绍【转】
kernel_init-->kernel_init_freeable:
static noinline void __init kernel_init_freeable(void)
{
/*
* Wait until kthreadd is all set-up.
*/
wait_for_completion(&kthreadd_done);-------------------等待kthreadd_done完成量
...

do_basic_setup();---------------------------------------很多初始化需要kthread_create支持

...
}
Linux进程管理 (篇外)内核线程简要介绍【转】

内核中有一个线程kthreadd_task负责创建其他内核线程,这个线程的函数为kthreadd()。

Linux进程管理 (篇外)内核线程简要介绍【转】
int kthreadd(void *unused)
{
struct task_struct *tsk = current; /* Setup a clean context for our children to inherit. */
set_task_comm(tsk, "kthreadd");
ignore_signals(tsk);
set_cpus_allowed_ptr(tsk, cpu_all_mask);
set_mems_allowed(node_states[N_MEMORY]); current->flags |= PF_NOFREEZE; for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (list_empty(&kthread_create_list))
schedule();----------------------------------------------如果kthread_create_list为空,让出CPU,进入休眠状态。在kthread_create_on_node()中会将要创建进程节点加入到kthread_create_list中,然后唤醒此进程。
__set_current_state(TASK_RUNNING); spin_lock(&kthread_create_lock);
while (!list_empty(&kthread_create_list)) {------------------只要kthread_create_list不为空,遍历kthread_create_list链表
struct kthread_create_info *create; create = list_entry(kthread_create_list.next,
struct kthread_create_info, list);
list_del_init(&create->list);----------------------------从kthread_create_list中摘除当前create
spin_unlock(&kthread_create_lock); create_kthread(create);----------------------------------创建线程 spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
} return 0;
} static void create_kthread(struct kthread_create_info *create)
{
int pid; #ifdef CONFIG_NUMA
current->pref_node_fork = create->node;
#endif
/* We want our own signal handler (we take no signals by default). */
pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);----调用do_fork()创建线程
if (pid < 0) {
/* If user was SIGKILLed, I release the structure. */
struct completion *done = xchg(&create->done, NULL); if (!done) {
kfree(create);
return;
}
create->result = ERR_PTR(pid);
complete(done);--------------------------------------------------------触发complete事件
}
} pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL);
}
Linux进程管理 (篇外)内核线程简要介绍【转】

2.2 创建内核线程接口:kthread_create等

kthread_create()是最常见的创建内核线程的接口。

kthread_create_on_cpu()相对于kthread_create多了个cpu,但都基于kthread_create_on_node()。

kthread_run基于kthreadd_create,所以这些函数都基于kthread_create_on_node。

Linux进程管理 (篇外)内核线程简要介绍【转】
#define kthread_create(threadfn, data, namefmt, arg...) \kthread_create_on_node(threadfn, data, -1, namefmt, ##arg)

struct task_struct *kthread_create_on_cpu(int (*threadfn)(void *data),
void *data,
unsigned int cpu,
const char *namefmt); /**
* kthread_run - create and wake a thread.
* @threadfn: the function to run until signal_pending(current).
* @data: data ptr for @threadfn.
* @namefmt: printf-style name for the thread.
*
* Description: Convenient wrapper for kthread_create() followed by
* wake_up_process(). Returns the kthread or ERR_PTR(-ENOMEM).
*/
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
=kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \--------------------------如果kthread_create()正确创建了一个进程,调用wake_up_process()唤醒它。
wake_up_process(__k); \
__k; \
})
Linux进程管理 (篇外)内核线程简要介绍【转】

kthread_create_on_node()负责创建一个线程,填充一个kthread_create_info结构体;然后将此结构体作为一个节点插入kthread_create_list队尾。

然后唤醒kthreadd_task进行处理,创建线程。

Linux进程管理 (篇外)内核线程简要介绍【转】
struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
void *data, int node,
const char namefmt[],
...)
{
DECLARE_COMPLETION_ONSTACK(done);
struct task_struct *task;
struct kthread_create_info *create = kmalloc(sizeof(*create),
GFP_KERNEL);---------------------------------创建插入kthread_create_list的节点。 if (!create)
return ERR_PTR(-ENOMEM);
create->threadfn = threadfn;
create->data = data;
create->node = node;
create->done = &done; spin_lock(&kthread_create_lock);
list_add_tail(&create->list, &kthread_create_list);-------------------将填充的节点插入kthread_create_list中。
spin_unlock(&kthread_create_lock); wake_up_process(kthreadd_task);---------------------------------------唤醒kthread_task处理kthread_create_list链表,创建相应的线程。
/*
* Wait for completion in killable state, for I might be chosen by
* the OOM killer while kthreadd is trying to allocate memory for
* new kernel thread.
*/
if (unlikely(wait_for_completion_killable(&done))) {------------------等待complete事件触发,在create_kthread()中触发。
/*
* If I was SIGKILLed before kthreadd (or new kernel thread)
* calls complete(), leave the cleanup of this structure to
* that thread.
*/
if (xchg(&create->done, NULL))
return ERR_PTR(-EINTR);
/*
* kthreadd (or new kernel thread) will call complete()
* shortly.
*/
wait_for_completion(&done);---------------------------------------等待complete事件触发。
}
task = create->result;------------------------------------------------创建的结果为task_struct结构体。
if (!IS_ERR(task)) {
static const struct sched_param param = { .sched_priority = 0 };
va_list args; va_start(args, namefmt);
vsnprintf(task->comm, sizeof(task->comm), namefmt, args);---------配置进程名称。
va_end(args);
/*
* root may have changed our (kthreadd's) priority or CPU mask.
* The kernel thread should not inherit these properties.
*/
sched_setscheduler_nocheck(task, SCHED_NORMAL, &param);-----------设置进程调度策略为NORMAL,优先级为0。
set_cpus_allowed_ptr(task, cpu_all_mask);
}
kfree(create);--------------------------------------------------------释放kthread_create_info。
return task;
}
Linux进程管理 (篇外)内核线程简要介绍【转】

3. 内核线程和普通线程的区别

内核线程没有地址空间,所以task_struct->mm指针为NULL。内核线程没有用户上下文。

内核线程只工作在内核空间,不会切换至用户空间。但内核线程同样是可调度且可抢占的。

普通线程即可工作在内核空间,也可工作在用户空间。

内核线程只能访问3GB以上地址,而普通线程可访问所有4GB地址空间。

4. irq、softirq、woker内核线程

irq、softirq、worker都可能创建对应的内核线程,有线程就有优先级。

下面从优先来来看看它们的重要性。

可以看出中断内核线程优先级很高,为49,并且使用了实时调度策略。softirq和worker都是普通内核线程。

  prio policy
irq 49 SCHED_FIFO
softirq 120 SCHED_NORMAL
worker 120 SCHED_NORMAL
init 120 SCHED_NORMAL
kthreadd 120 SCHED_NORMAL
cfinteractive 0 SCHED_FIFO

其它特殊内核线程init优先级为120,kthreadd优先级为120.

cfinteractive优先级最高,主要处理CPU Frequency负载更新。

4.1 irq/xx-xx:创建处理线程化中断的线程

request_threaded_irq-->__setup_irq,可见如果设置了thread_fn,并且不允许中断嵌套,则创建一个类似"irq/中断号-终端名称"的线程。

线程函数是irq_thread,

Linux进程管理 (篇外)内核线程简要介绍【转】
/*
* Internal function to register an irqaction - typically used to
* allocate special interrupts that are part of the architecture.
*/
static int
__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
...
if (new->thread_fn && !nested) {
struct task_struct *t;
static const struct sched_param param = {
.sched_priority = MAX_USER_RT_PRIO/2,
}; t = kthread_create(irq_thread, new, "irq/%d-%s", irq,----------------在irq_thread中调用irq_thread_fn,进而调用action->thread_fn,request_threaded_irq参数thread_fn。
new->name);
...
}
...
}
Linux进程管理 (篇外)内核线程简要介绍【转】

request_irq是对request_threaded_irq的封装,创建中断线程的工作交给__setup_irq()

Linux进程管理 (篇外)内核线程简要介绍【转】
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
Linux进程管理 (篇外)内核线程简要介绍【转】

更详细信息参考:《Linux中断管理 (1)Linux中断管理机制》中关于request_irq()介绍。

4.2 ksoftirqd/xx:创建处理软中断线程

软中断线程通过smpboot_register_percpu_thread注册softirq_threads创建。

Linux进程管理 (篇外)内核线程简要介绍【转】
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
}; static __init int spawn_ksoftirqd(void)
{
register_cpu_notifier(&cpu_nfb); BUG_ON(smpboot_register_percpu_thread(&softirq_threads)); return 0;
}
Linux进程管理 (篇外)内核线程简要介绍【转】

smpboot_register_percpu_thread-->__smpboot_create_thread,最终也还是调用kthread_create_on_cpu,创建了类似"ksoftirqd/xx"的内核线程,xx为cpuid号。

从ps -a中可以看出创建的结果如下,可以看出每个CPU创建了一个ksoftirqd内核线程。

    3 0          0:03 [ksoftirqd/0]
11 0 0:03 [ksoftirqd/1]
15 0 0:00 [ksoftirqd/2]
19 0 0:00 [ksoftirqd/3]

更详细信息参考:  《Linux中断管理 (2)软中断和tasklet

4.3 kworker:创建work的工作线程

kwoker线程是处理work的工作线程,详细参考《Linux中断管理 (3)workqueue工作队列》。

每个CPU都会创建自己的workqueue,用以集中处理内核kworker。

workquuue就是把一些任务(work)推迟到一个或一组内核线程中去执行,那个内核线程被称为worker_thread。

首先看看创建结果,可以看出在init_workqueues中创建了绑定CPU0的两个kworker,分别是nice=0和nice=-20。

apply_workqueue_attrs创建unbund worker,即kworker/u8:0。

然后在每个CPU_UP_PREPARE回调中创建两个不同nice的kworker。所以四个CPU一共9个内核线程。

Linux进程管理 (篇外)内核线程简要介绍【转】
PID   USER     TIME   COMMAND
1 0 0:01 {linuxrc} init
2 0 0:00 [kthreadd]
3 0 0:00 [ksoftirqd/0]
4 0 0:00 [kworker/0:0]
5 0 0:00 [kworker/0:0H]---------------init_workqueues-->create_worker
6 0 0:00 [kworker/u8:0]---------------apply_workqueue_attrs-->alloc_unbound_pwq-->create_worker
7 0 0:00 [rcu_sched]
8 0 0:00 [rcu_bh]
9 0 0:00 [migration/0]
10 0 0:00 [migration/1]
11 0 0:00 [ksoftirqd/1]
12 0 0:00 [kworker/1:0]---------------workqueue_cpu_up_callback-->create_worker
13 0 0:00 [kworker/1:0H]
14 0 0:00 [migration/2]
15 0 0:00 [ksoftirqd/2]
16 0 0:00 [kworker/2:0]
17 0 0:00 [kworker/2:0H]--------------workqueue_cpu_up_callback-->create_worker
18 0 0:00 [migration/3]
19 0 0:00 [ksoftirqd/3]
20 0 0:00 [kworker/3:0]
21 0 0:00 [kworker/3:0H]--------------workqueue_cpu_up_callback-->create_worker
22 0 0:00 [khelper]
23 0 0:00 [kdevtmpfs]
24 0 0:00 [perf]
25 0 0:00 [kworker/u8:1]--------------worker_thread-->create_worker
279 0 0:00 [khungtaskd]
280 0 0:00 [writeback]
281 0 0:00 [kintegrityd]
282 0 0:00 [kworker/0:1]---------------worker_thread-->create_worker
284 0 0:00 [bioset]
286 0 0:00 [kblockd]
294 0 0:00 [ata_sff]
408 0 0:00 [rpciod]
409 0 0:00 [kworker/2:1]---------------worker_thread-->create_worker
410 0 0:00 [kworker/1:1]---------------worker_thread-->create_worker
412 0 0:00 [kswapd0]
416 0 0:00 [fsnotify_mark]
429 0 0:00 [nfsiod]
449 0 0:00 [kworker/3:1]---------------worker_thread-->create_worker
527 0 0:00 [kpsmoused]
537 0 0:00 [kworker/1:2]---------------worker_thread-->create_worker
613 0 0:00 [deferwq]
Linux进程管理 (篇外)内核线程简要介绍【转】

init_workqueues-->create_worker-->kthread_create_on_node,创建"kworker/xx:xxH"内核线程。

Linux进程管理 (篇外)内核线程简要介绍【转】
static int __init init_workqueues(void)
{
int std_nice[NR_STD_WORKER_POOLS] = { 0, HIGHPRI_NICE_LEVEL };
int i, cpu;
...
/* create the initial worker */
for_each_online_cpu(cpu) {---------------------------------遍历CPU[0~3]
struct worker_pool *pool; for_each_cpu_worker_pool(pool, cpu) {------------------NR_STD_WORKER_POOLS=2,所以每个CPU有两个pool
pool->flags &= ~POOL_DISASSOCIATED;
BUG_ON(!create_worker(pool));
}
}
...
system_wq = alloc_workqueue("events", 0, 0);
system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
system_long_wq = alloc_workqueue("events_long", 0, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,
WQ_UNBOUND_MAX_ACTIVE);
system_freezable_wq = alloc_workqueue("events_freezable",
WQ_FREEZABLE, 0);
system_power_efficient_wq = alloc_workqueue("events_power_efficient",
WQ_POWER_EFFICIENT, 0);
system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient",
WQ_FREEZABLE | WQ_POWER_EFFICIENT,
0);
BUG_ON(!system_wq || !system_highpri_wq || !system_long_wq ||
!system_unbound_wq || !system_freezable_wq ||
!system_power_efficient_wq ||
!system_freezable_power_efficient_wq);
return 0;
}
Linux进程管理 (篇外)内核线程简要介绍【转】

create_worker()函数创建工作线程。

Linux进程管理 (篇外)内核线程简要介绍【转】
static struct worker *create_worker(struct worker_pool *pool)
{
...
if (pool->cpu >= 0)
snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,-------------cpuid和id,区分cpu和cpu内kworker。
pool->attrs->nice < 0 ? "H" : "");
else
snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);--------------u表示不指定cpu。 worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
"kworker/%s", id_buf);
...
}
Linux进程管理 (篇外)内核线程简要介绍【转】

更详细信息参考:《Linux中断管理 (3)workqueue工作队列》、《Linux workqueue工作原理》、《Concurrency Managed Workqueue之(一):workqueue的基本概念

5. 其他内核线程

rcu_sched、rcu_bh

migration

khelper

kdevtmpfs

perf

writeback

kintegrityd

bioset

kblockd

ata_sff

rpciod

kswapd

nfsiod

kpsmpused

deferwq

联系方式:arnoldlu@qq.com

Linux进程管理 (篇外)内核线程简要介绍【转】的更多相关文章

  1. Linux进程管理 &lpar;篇外&rpar;内核线程简要介绍

    关键词:kthread.irq.ksoftirqd.kworker.workqueues 在使用ps查看线程的时候,会有不少[...]名称的线程,这些有别于其它线程,都是内核线程. 其中多数内核线程从 ...

  2. Linux进程管理(三、 线程)

    // ---- refer glibc, pthread_create.c ----// int __pthread_create_2_0 (newthread, attr, start_routin ...

  3. Linux进程管理专题

    Linux进程管理 (1)进程的诞生介绍了如何表示进程?进程的生命周期.进程的创建等等? Linux支持多种调度器(deadline/realtime/cfs/idle),其中CFS调度器最常见.Li ...

  4. Linux进程管理 &lpar;1&rpar;进程的诞生

    专题:Linux进程管理专题 目录: Linux进程管理 (1)进程的诞生 Linux进程管理 (2)CFS调度器 Linux进程管理 (3)SMP负载均衡 Linux进程管理 (4)HMP调度器 L ...

  5. Linux进程管理 &lpar;2&rpar;CFS调度器

    关键词: 目录: Linux进程管理 (1)进程的诞生 Linux进程管理 (2)CFS调度器 Linux进程管理 (3)SMP负载均衡 Linux进程管理 (4)HMP调度器 Linux进程管理 ( ...

  6. Linux进程管理子系统分析【转】

    本文转载自:http://blog.csdn.net/coding__madman/article/details/51298732 Linux进程管理: 进程与程序: 程序:存放在磁盘上的一系列代码 ...

  7. Linux进程管理知识整理

    Linux进程管理知识整理 1.进程有哪些状态?什么是进程的可中断等待状态?进程退出后为什么要等待调度器删除其task_struct结构?进程的退出状态有哪些? TASK_RUNNING(可运行状态) ...

  8. Linux 进程管理剖析--转

    地址:http://www.ibm.com/developerworks/cn/linux/l-linux-process-management/index.html Linux 是一种动态系统,能够 ...

  9. Linux性能及调优指南(翻译)之Linux进程管理

    本文为IBM RedBook的Linux Performanceand Tuning Guidelines的1.1节的翻译原文地址:http://www.redbooks.ibm.com/redpap ...

随机推荐

  1. 移动端IOS点击事件失效解决方案

    解决方案 解决办法有 4 种可供选择: 1 将 click 事件直接绑定到目标元素(即 .target)上 2 将目标元素换成 <a> 或者 button 等可点击的元素 3 将 clic ...

  2. 初识servlet--未完成

    servlet到底是什么呢?今天我们一起看一下. 首先说下servlet是干啥的.servlet主要干这么一个事情:创建动态的问页面. servlet主要的功能是,访问外网,连接外部接口.它可以做如下 ...

  3. 201521123066 《Java程序设计》第三周学习总结

    1. 本周学习总结 初学面向对象,会学习到很多碎片化的概念与知识.尝试学会使用思维导图将这些碎片化的概念.知识组织起来.请使用纸笔或者下面的工具画出本周学习到的知识点.截图或者拍照上传. 2. 书面作 ...

  4. Exp3 免杀原理与实践 20164302 王一帆

    1 实践内容 1.1 正确使用msf编码器(0.5分),msfvenom生成如jar之类的其他文件(0.5分),veil-evasion(0.5分),加壳工具(0.5分),使用shellcode编程( ...

  5. linux下的QT打包方法

    一句话很简单,一个shell脚本搞定,不跟你嘻嘻哈哈 #!/bin/shexe="ThorIceLocker"#存放你的可执行文件的名字des="/home/ninetr ...

  6. 【题解】放球游戏B

    题目描述 校园里在上活动课,Red和Blue两位小朋友在玩一种游戏,他俩在一排N个格子里,自左到右地轮流放小球,每个格子只能放一个小球.第一个人只能放1个球,之后的人最多可以放前一个人的两倍数目的球, ...

  7. APScheduler

    目录 APScheduler简介 支持的后端存储作业 集成的Python框架 APScheduler下载安装 APScheduler组件 各组件简介 调度器 作业存储器 执行器 触发器 使用 添加作业 ...

  8. MVC框架与三层架构

    MVC框架 介绍: MVC全名Model View Controller Model:模型的意思,代表业务模型 View:视图的意思,代表用户界面 Controller:控制器的意思,控制器接受用户的 ...

  9. libvirt虚拟库

    转载自:https://www.ibm.com/developerworks/cn/linux/l-libvirt/index.html Libvirt 虚拟化库剖析   讲到向外扩展计算(比如云计算 ...

  10. RDLC 设置表的重复标题行(在每页中显示标题行)

    在Tablix 属性对话框中勾选“在每一页上重复标题行”及“滚动时保持标题可见”结果没有用. 如果您使用的是一个“表”式布局,有一个简单的方法,可以尝试以下四个步骤: 1.在分组窗格中,单击窗格上的小 ...