一 、Linux内核创建一个新进程的过程

1. 知识准备

  • 操作系统内核三大功能是进程管理,内存管理,文件系统,最核心的是进程管理
  • linux 进程的状态和操作系统原理的描述进程状态有所不同,比如就绪状态和运行状态都是TASK_RUNNING。(这个表示它是可运行的,但是实际上有没有在运行取决于它是否占有 CPU )
  • fork 被调用一次,能够返回两次。在父进程中返回新创建子进程的 pid;在子进程中返回 0
  • 调用 fork 之后,数据、堆、栈有两份,代码仍然为一份(这个代码段成为两个进程的共享代码段)。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。

2. 内核代码分析

SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
return -EINVAL;
#endif
}
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif

通过上面的代码可以看出 fork、vfork 和 clone 三个系统调用都可以创建一个新进程,而且都是通过 do_fork 来创建进程,只不过传递的参数不同。

(1)do_fork

long do_fork(unsigned long clone_flags, unsigned long stack_start,
unsigned long stack_size, int __user *parent_tidptr,
int __user *child_tidptr)

首先了解一下 do_fork () 的参数:

  • clone_flags:子进程创建相关标志,通过此标志可以对父进程的资源进行有选择的复制。

  • stack_start:子进程用户态堆栈的地址。

  • regs:指向 pt_regs 结构体(当系统发生系统调用时,pt_regs 结构体保存寄存器中的值并按顺序压入内核栈)的指针。

  • stack_size:用户态栈的大小,通常是不必要的,总被设置为0。

  • parent_tidptr 和 child_tidptr:父进程、子进程用户态下 pid 地址。

为方便理解,下述为精简关键代码:

  struct task_struct *p;    //创建进程描述符指针
int trace = 0;
long nr; //子进程pid
...
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace); //创建子进程的描述符和执行时所需的其他数据结构 if (!IS_ERR(p)) //如果 copy_process 执行成功
struct completion vfork; //定义完成量(一个执行单元等待另一个执行单元完成某事)
struct pid *pid;
...
pid = get_task_pid(p, PIDTYPE_PID); //获得task结构体中的pid
nr = pid_vnr(pid); //根据pid结构体中获得进程pid
...
// 如果 clone_flags 包含 CLONE_VFORK 标志,就将完成量 vfork 赋值给进程描述符中的vfork_done字段,此处只是对完成量进行初始化
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
} wake_up_new_task(p); //将子进程添加到调度器的队列,使之有机会获得CPU /* forking complete and child started to run, tell ptracer */
...
// 如果 clone_flags 包含 CLONE_VFORK 标志,就将父进程插入等待队列直至程直到子进程释调用exec函数或退出,此处是具体的阻塞
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
} put_pid(pid);
} else {
nr = PTR_ERR(p); //错误处理
}
return nr; //返回子进程pid(父进程的fork函数返回的值为子进程pid的原因)
}

do_fork()主要完成了调用 copy_process() 复制父进程信息、获得pid、调用 wake_up_new_task 将子进程加入调度器队列,为之分配 CPU、通过 clone_flags 标志做一些辅助工作。其中 copy_process()是创建一个进程内容的主要的代码。

(2)copy_process

static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;
...
retval = security_task_create(clone_flags);//安全性检查
...
p = dup_task_struct(current); //复制PCB,为子进程创建内核栈、进程描述符
ftrace_graph_init_task(p);
··· retval = -EAGAIN;
// 检查该用户的进程数是否超过限制
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
// 检查该用户是否具有相关权限,不一定是root
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}
...
// 检查进程数量是否超过 max_threads,后者取决于内存的大小
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count; if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;
...
spin_lock_init(&p->alloc_lock); //初始化自旋锁
init_sigpending(&p->pending); //初始化挂起信号
posix_cpu_timers_init(p); //初始化CPU定时器
···
retval = sched_fork(clone_flags, p); //初始化新进程调度程序数据结构,把新进程的状态设置为TASK_RUNNING,并禁止内核抢占
...
// 复制所有的进程信息
shm_init_task(p);
retval = copy_semundo(clone_flags, p);
...
retval = copy_files(clone_flags, p);
...
retval = copy_fs(clone_flags, p);
...
retval = copy_sighand(clone_flags, p);
...
retval = copy_signal(clone_flags, p);
...
retval = copy_mm(clone_flags, p);
...
retval = copy_namespaces(clone_flags, p);
...
retval = copy_io(clone_flags, p);
...
retval = copy_thread(clone_flags, stack_start, stack_size, p);// 初始化子进程内核栈
...
//若传进来的pid指针和全局结构体变量init_struct_pid的地址不相同,就要为子进程分配新的pid
if (pid != &init_struct_pid) {
retval = -ENOMEM;
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
if (!pid)
goto bad_fork_cleanup_io;
} ...
p->pid = pid_nr(pid); //根据pid结构体中获得进程pid
//若 clone_flags 包含 CLONE_THREAD标志,说明子进程和父进程在同一个线程组
if (clone_flags & CLONE_THREAD) {
p->exit_signal = -1;
p->group_leader = current->group_leader; //线程组的leader设为子进程的组leader
p->tgid = current->tgid; //子进程继承父进程的tgid
} else {
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p; //子进程的组leader就是它自己 p->tgid = p->pid; //组号tgid是它自己的pid
} ... if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace); init_task_pid(p, PIDTYPE_PID, pid);
if (thread_group_leader(p)) {
...
// 将子进程加入它所在组的哈希链表中
attach_pid(p, PIDTYPE_PGID);
attach_pid(p, PIDTYPE_SID);
__this_cpu_inc(process_counts);
} else {
...
}
attach_pid(p, PIDTYPE_PID);
nr_threads++; //增加系统中的进程数目
}
...
return p; //返回被创建的子进程描述符指针P
...
}

copy_process 主要完成了调用 dup_task_struct 复制当前的 task_struct、信息检查、初始化、把进程状态设置为 TASK_RUNNING、复制所有进程信息、调用 copy_thread 初始化子进程内核栈、设置子进程pid。

(3)dup_task_struct

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int node = tsk_fork_get_node(orig);
int err;
tsk = alloc_task_struct_node(node); //为子进程创建进程描述符
...
ti = alloc_thread_info_node(tsk, node); //实际上是创建了两个页,一部分用来存放 thread_info,一部分就是内核堆栈
...
err = arch_dup_task_struct(tsk, orig); //复制父进程的task_struct信息
...
tsk->stack = ti; // 将栈底的值赋给新结点的stack setup_thread_stack(tsk, orig);//对子进程的thread_info结构进行初始化(复制父进程的thread_info 结构,然后将 task 指针指向子进程的进程描述符)
...
return tsk; // 返回新创建的进程描述符指针
...
}

(4)copy_thread

dup_task_struct 只是为子进程创建一个内核栈,copy_thread 才真正完成赋值。

int copy_thread(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p)
{ struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err; p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps)); if (unlikely(p->flags & PF_KTHREAD)) {
/* kernel thread */
memset(childregs, 0, sizeof(struct pt_regs)); p->thread.ip = (unsigned long) ret_from_kernel_thread; //如果创建的是内核线程,则从ret_from_kernel_thread开始执行
task_user_gs(p) = __KERNEL_STACK_CANARY;
childregs->ds = __USER_DS;
childregs->es = __USER_DS;
childregs->fs = __KERNEL_PERCPU;
childregs->bx = sp; /* function */
childregs->bp = arg;
childregs->orig_ax = -1;
childregs->cs = __KERNEL_CS | get_kernel_rpl();
childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
p->thread.io_bitmap_ptr = NULL;
return 0;
} *childregs = *current_pt_regs();//复制内核堆栈(复制父进程的寄存器信息,即系统调用SAVE_ALL压栈的那一部分内容) childregs->ax = 0; //子进程的eax置为0,所以fork的子进程返回值为0
...
p->thread.ip = (unsigned long) ret_from_fork;//ip指向 ret_from_fork,子进程从此处开始执行
task_user_gs(p) = get_user_gs(current_pt_regs());
...
return err;

4. gdb调试

在刚才分析的关键点处分别设置断点:

现在 sys_clone 停下,再在 do_fork 停下,继续单步执行:

继续在 copy_process 停下,在 copy_thread 处停下,在这个地方可以查看 p 的值:

最后 ret_from_fork 跟踪到 syscall_exit 后无法继续。

5. 遇到的问题及解决

(1) thread_info 是什么?

通过搜索得知,它被称为小型的进程描述符,内存区域大小是8KB,占据连续的两个页框。该结构通过 task 指针指向进程描述符。内核栈是由高地址到低地址增长,thread_info 结构由低地址到高地址增长。内核通过屏蔽 esp 的低13位有效位获得 thread_info 结构的基地址。内核栈、thread_info结构、进程描述符之间的关系如下图所示(在较新的内核代码中,task_struct 结构中没有直接指向 thread_info 结构的指针,而是用一个 void 指针类型的成员表示,然后通过类型转换来访问 thread_info 结构)。

内核栈和 thread_info 结构是被定义在一个联合体当中,alloc_thread_info_node 分配的实则是一个联合体,即既分配了 thread_info 结构又分配了内核栈。

union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

我们想要获得的一般是进程描述符而不是 thread_info,可以用 current 宏获取进程描述符(用task指针找到进程描述符)

static inline struct task_struct * get_current(void)
{
return current_thread_info()->task;
}

(2)do_fork 中,pid = get_task_pid(p, PIDTYPE_PID)不是就获取了 pid值吗?怎么后面还有一句 nr = pid_vnr(pid) ?

参考Linux 内核进程管理之进程ID,了解到PID命名空间相关知识。

pid结构体:

struct pid {
struct hlist_head tasks; //指回 pid_link 的 node
int nr; //PID
struct hlist_node pid_chain; //pid hash 散列表结点
};

pid_vnr:

 pid_t pid_vnr(struct pid*pid)
{
return pid_nr_ns(pid,current->nsproxy->pid_ns); //current->nsproxy->pid_ns是当前pid_namespace
}

获得 pid 实例之后,再根据 pid 中的numbers 数组中 uid 信息,获得局部PID。

pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
{
struct upid *upid;
pid_t nr = 0; if (pid && ns->level <= pid->level) {
upid = &pid->numbers[ns->level];
if (upid->ns == ns)
nr = upid->nr;
}
return nr;
}

由于PID命名空间的层次性,父命名空间能看到子命名空间的内容,反之则不能。因此函数中需要确保当前命名空间的level 小于等于产生局部PID的命名空间的level(全局ID:在内核本身和初始命名空间中唯一的ID,在系统启动期间开始的 init 进程即属于该初始命名空间。系统中每个进程都对应了该命名空间的一个PID,叫全局ID,保证在整个系统中唯一;局部ID:对于属于某个特定的命名空间,它在其命名空间内分配的ID为局部ID,该ID也可以出现在其他的命名空间中)。

二 、课本笔记

1.定时器和时间管理

  • 内核在硬件的帮助下计算和管理时间。
  • 系统定时器以某种频率自行触发时钟中断,称为节拍率。
  • 连续两次时钟中断的间隔时间,成为节拍(节拍率分之一)
  • 墙上时间(实际时间)和系统运行时间(自系统启动开始所经的时间)根据时钟间隔来计算。
  • 全局变量jiffies用来记录自系统启动以来产生的节拍总数。启动时内核将它初始化为0,此后每次时钟中断处理程序增加该变量的值。每一秒钟中断次数HZ,jiffies一秒内增加的值就是HZ,系统运行时间(以秒为单位) 为 jiffie/HZ。
  • 比较时间的几个宏:
time_after(unknown, known)      //unknown after known ? true : false;
time_before(unknown, known) //unknown before known ? true : false;
time_after_eq(unknown, known) //unknown after or eq known ? true : false;
time_before_eq(unknown, known) //unknown before or eq known ? true : false;
  • 实时时钟 RTC 是用来持久存放系统时间的设备.
  • 定时器是管理内核流逝的时间的基础。只需执行初始化工作,设置一个超时时间,指定超时发生后执行的函数,然后激活定时器就可以了。定时器不周期运行,它在超时后自行撤退。定时器由以下结构表示:
struct timer_list {
struct list_head entry;//定时器链表的入口
unsigned long expires;//基于jiffies的定时值
struct tvec_base *base;//定时器内部值
void (*function)(unsigned long);//定时器处理函数
...
};

定时器处理函数的函数原型:

void my_timer_function(unsigned long data);

add_timer(&my_timer);            //激活定时器

mod_timer(&my_timer, jiffies + new_dalay);        //改变指定定时器的超时时间
//如果定时器未被激活,mod_timer会激活该定时器
//如果调用时定时器未被激活,该函数返回0;否则返回1. del_timer(&my_timer); //在定时器超时前停止定时器
//被激活或未被激活的定时器都可以使用该函数
//如果调用时定时器未被激活,该函数返回0;否则返回1.
//不需要为已经超时的定时器调用该函数,因为他们会自动删除
  • 延迟执行不应该在持有锁时或禁止中断时发生
  • 最简单的延迟方法是忙等待(延迟时间是节拍的整数倍或者精确率要求不高可以使用)
  • 短延迟的延迟时间精确到毫秒,微妙;短暂等待某个动作完成时,比时钟节拍更短,需要依靠数次循环达到延迟效果。
  • schedule_timeout() 使执行的任务睡眠指定时间达到延迟.调用它的代码必须处于进程上下文中,并且不能持有锁。
set_current_state(state);        //将任务设置为可中断睡眠状态或不可中断睡眠状态
schedule_timeout(s*HZ); //S秒后唤醒,被延迟的任务并将其重新放回运行队列。

2.内存管理

  • 虚拟地址又叫线性地址。linux没有采用分段机制,所以逻辑地址和虚拟地址(线性地址)是一个概念。内核的虚拟地址和物理地址大部分只差一个线性偏移量。用户空间的虚拟地址和物理地址则采用了多级页表进行映射,但仍称之为线性地址。
  • 在x86结构中,Linux内核虚拟地址空间划分03G为用户空间,34G为内核空间。内核把物理页作为内存管理的基本单元,内核虚拟空间(3G~4G)把页又划分为三种类型的区:
ZONE_DMA 3G之后起始的16MB
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~1G
  • 由于内核的虚拟和物理地址只差一个偏移量:物理地址 = 逻辑地址 – 0xC0000000。所以如果1G内核空间完全用来线性映射,物理内存也只能访问到1G区间。HIGHMEM可以解决此问题,专门开辟的一块不必线性映射,可以灵活定制映射,以便访问1G以上物理内存的区域。

  • 最核心的分配页面函数为:
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)
  • 给定的页转为逻辑地址:
void * page_address(struct page * page)
  • 初始化为0的页面:
unsigned long get_zeroed_page(unsigned int gfp_mask)
  • 释放页,只能释放属于你的页:
void _free_pages(struct page * page, unsigned int order) //释放page结构体指向的连续2的order次方个页面
void free_pages(unsigned long addr, unsigned int order) //释放从地址addr开始的,连续2的order次方个页面
void free_page(unsigned long addr) //释放地址addr的一个页
  • kmalloc分配的内存虚拟、物理地址都是连续的,vmalloc分配的内存虚拟地址连续,而物理地址无需连续
  • 当代码需要一个新的数据结构的实例时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是释放它。
  • slab分配器为每种对象分配一个高速缓存,这个缓存可以看做是同类型对象的一种储备。每个高速缓存所占的内存区又被划分多个slab,每个 slab是由一个或多个连续的页框组成。每个页框中包含若干个对象,既有已经分配的对象,也包含空闲的对象。

  • 每个高速缓存通过kmem_cache结构来描述,这个结构中包含了对当前高速缓存各种属性信息的描述。所有的高速缓存通过双链表组织在一起,形成 高速缓存链表cache_chain。每个kmem_cache结构中并不包含对具体slab的描述,而是通过kmem_list3结构组织各个 slab。该结构的定义如下:
 struct kmem_list3 {
struct list_head slabs_partial; //包含空闲对象和已经分配对象的slab描述符
struct list_head slabs_full;//只包含非空闲的slab描述符
struct list_head slabs_free;//只包含空闲的slab描述符
unsigned long free_objects; /*高速缓存中空闲对象的个数*/
unsigned int free_limit; //空闲对象的上限
unsigned int colour_next; /* Per-node cache coloring *//*即将要着色的下一个*/
spinlock_t list_lock;
struct array_cache *shared; /* shared per node */
struct array_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking *//**/
int free_touched; /* updated without locking */
};
  • 每个slab处于三个状态之一:满、部分满、空。非空闲对象的slab链表 slabs_full、部分空闲对象的slab链表slabs_partial、空闲对象的slab链表slabs_free。当内核的某一部分需要一个新的对象时,先从 slabs_partial 分配,没有部分满的slab,就从 slabs_free 中分配。若没有空的 slab,就要创建一个 slab。

参考:slab机制Linux内存管理中的slab分配器

2017-2018-1 20179202《Linux内核原理与分析》第七周作业的更多相关文章

  1. 20169212《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 这一周学习了MOOCLinux内核分析的第一讲,计算机是如何工作的?由于本科对相关知识的不熟悉,所以感觉有的知识理解起来了有一定的难度,不过多查查资 ...

  2. 20169210《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 本周作业分为两部分:第一部分为观看学习视频并完成实验楼实验一:第二部分为看<Linux内核设计与实现>1.2.18章并安装配置内核. 第 ...

  3. 2018-2019-1 20189221 《Linux内核原理与分析》第九周作业

    2018-2019-1 20189221 <Linux内核原理与分析>第九周作业 实验八 理理解进程调度时机跟踪分析进程调度与进程切换的过程 进程调度 进度调度时机: 1.中断处理过程(包 ...

  4. 2017-2018-1 20179215《Linux内核原理与分析》第二周作业

    20179215<Linux内核原理与分析>第二周作业 这一周主要了解了计算机是如何工作的,包括现在存储程序计算机的工作模型.X86汇编指令包括几种内存地址的寻址方式和push.pop.c ...

  5. 2019-2020-1 20199329《Linux内核原理与分析》第九周作业

    <Linux内核原理与分析>第九周作业 一.本周内容概述: 阐释linux操作系统的整体构架 理解linux系统的一般执行过程和进程调度的时机 理解linux系统的中断和进程上下文切换 二 ...

  6. 2019-2020-1 20199329《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 一.上周问题总结: 未能及时整理笔记 Linux还需要多用 markdown格式不熟练 发布博客时间超过规定期限 二.本周学习内容: <庖丁解 ...

  7. 2019-2020-1 20209313《Linux内核原理与分析》第二周作业

    2019-2020-1 20209313<Linux内核原理与分析>第二周作业 零.总结 阐明自己对"计算机是如何工作的"理解. 一.myod 步骤 复习c文件处理内容 ...

  8. 2018-2019-1 20189221《Linux内核原理与分析》第一周作业

    Linux内核原理与分析 - 第一周作业 实验1 Linux系统简介 Linux历史 1991 年 10 月,Linus Torvalds想在自己的电脑上运行UNIX,可是 UNIX 的商业版本非常昂 ...

  9. 《Linux内核原理与分析》第一周作业 20189210

    实验一 Linux系统简介 这一节主要学习了Linux的历史,Linux有关的重要人物以及学习Linux的方法,Linux和Windows的区别.其中学到了LInux中的应用程序大都为开源自由的软件, ...

  10. 2018-2019-1 20189221《Linux内核原理与分析》第二周作业

    读书报告 <庖丁解牛Linux内核分析> 第 1 章 计算工作原理 1.1 存储程序计算机工作模型 1.2 x86-32汇编基础 1.3汇编一个简单的C语言程序并分析其汇编指令执行过程 因 ...

随机推荐

  1. 2015/11/5用Python写游戏,pygame入门(5):面向对象的游戏设计

    昨天的内容里有了运动的子弹,虽然我们只添加了一个子弹,但你可以看到我们需要记录子弹的x,y坐标,每次要更新它的坐标.如果我们想要有多颗子弹,就需要存储多个坐标.那时候处理起来就不显得那么简单,也许我们 ...

  2. What Does “Neurons that Fire Together Wire Together” Mean?

    What Does “Neurons that Fire Together Wire Together” Mean? I’ve heard the phrase “neurons that fire ...

  3. ECMAScript6语法检查规范错误信息说明

    项目中使用ECMAScript6的时候经查会使用语法检查,下面是常见错误信息的汇总: “Missing semicolon.” : “缺少分号.”, “Use the function form of ...

  4. Github遇到Permanently added the RSA host key for IP address '52.74.223.119' to the list of known hosts.

    警告:为IP地址192.30.252.128的主机(RSA连接的)持久添加到hosts文件中,那就来添加吧! 解决办法: vim /etc/hosts 添加一行:52.74.223.119 githu ...

  5. [Node.js] querystring类

    和参数相关的帮助类,原生自带,直接 require('querystring') 即可使用. 此类一共包括4个方法: querystring.stringify(obj, [sep], [eq]) q ...

  6. 20155306 2016-2017-2 《Java程序设计》第6周学习总结

    20155306 2016-2017-2 <Java程序设计>第6周学习总结 教材学习内容总结 第十章 输入/输出 10.1 InputStream与OutputStream 如果要将数据 ...

  7. MFC基于对话框风格按钮控件添加图片的方法(大神止步)

    菜鸟还在研究这个东西,大神就不要看了.一直都在觉得用VC或VS建立的对话框总是全灰色感觉太单调了,如果可以在上面添加一些漂亮的图片就好了,今天终于实现了.其实挺简单的,下面就分几个步骤讲一下: 第一步 ...

  8. HDU 1556 Color the ball (树状数组 区间更新+单点查询)

    题目链接 Problem Description N个气球排成一排,从左到右依次编号为1,2,3....N.每次给定2个整数a b(a <= b),lele便为骑上他的"小飞鸽&quo ...

  9. 如何编写 Typescript 声明文件

    使用TypeScript已经有了一段时间,这的确是一个好东西,虽说在使用的过程中也发现了一些bug,不过都是些小问题,所以整体体验还是很不错的. TypeScript之所以叫Type,和它的强类型是分 ...

  10. VUE和ES6资源收集

    MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript https://developer.mozilla.org/en/docs/We ...