进程的定义

进程是指一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程

源代码在经过编译链接之后生成了可执行文件,再由操作系统进行加载并且进行一些堆栈的分配才是进程

进程控制块

操作系统管理控制进程运行所用的信息集合

  • 操作系统用PCB来描述进程的基本情况以及运行变化的过程

  • PCB是进程存在的唯一标志

    每个进程都在操作系统中有一个对应的PCB

进程控制块主要包含的就是进程的标识信息,处理机现场保存和进程控制信息

控制信息

  1. 调度和状态信息
  2. 进程间通信信息
  3. 存储管理信息
  4. 进程所用资源
  5. 有关数据结构连接信息

进程的生命周期

  • 进程创建

    用户请求创建一个新进程,正在运行的进程执行了创建进程的系统调用,并且加入到就绪队列

  • 进程执行

    内核对就绪队列进行调度,到执行该进程

  • 进程等待

    运行中的进程可能会进入阻塞状态,比如进行IO的等待或者需要的数据没有到达

  • 进程抢占

    运行中的进程可能时间片被用完或者高优先级进程被唤醒导致了进程被抢占进入阻塞状态

  • 进程唤醒

    被阻塞需要的资源可以被满足就可能被唤醒进入就绪队列

  • 进程结束

    进程完成任务或者被迫结束

线程的定义

线程是进程的一部分,描述指令流执行状态。它是进程中的指令执行流的最小单元,是CPU调度的基本单位。

  • 进程作为资源分配角色

进程由一组相关资源构成,包括地址空间(代码段、数据段)、打开的文件等各种资源

  • 线程作为处理机调度角色

线程描述在进程资源环境中的指令流执行状态

一个进程中可以同时存在多个线程,各个线程之间可以并发地执行,各个线程之间可以共享地址空间和文件等资源

线程的实现方式

用户线程

用户线程是基于在用户态自己实现的线程库函数来完成对线程的管理,包括线程的创建、终止、同步和调度

这样内核并不知道用户线程的存在,所以每个线程控制块都由线程库函数来维护,也不要在用户态和内核态进行切换,开销会较小,但是对于如果线程发生阻塞的话,由于操作系统并不知道用户级线程的情况,所以会造成整个进程的阻塞,并且除非当前运行线程主动放弃,它所在进程的其他线程无法抢占CPU

内核线程

由内核通过系统调用实现的线程机制,由内核完成线程的创建、终止和管理

由内核维护PCB和TCB,线程执行系统调用而被阻塞不影响其他线程,线程的创建、终止和切换相对较大,以线程为单位进行CPU时间分配

进程控制

进程切换

暂停当前运行进程,从运行状态变成其他状态,调度另一个进程从就绪状态变成运行状态

要完成进程切换就需要对切换前的进程进行进程上下文的保存,并且在切换后对进程上下文的恢复

进程控制块:内核为每个进程维护了对应的进程控制块(PCB),内核将相同状态的进程的PCB放置在同一队列

进程创建

fork/exec

  • fork() 创建一个继承的子进程

    复制父进程的所有变量和内存,复制父进程的所有CPU寄存器(有一个寄存器例外),fork()执行过程对于子进程而言,是在调用时间对父进程地址空间的一次复制,对于父进程fork() 返回child PID, 对于子进程返回值为0

  • fork()的地址空间复制

    fork()执行过程对于子进程而言,是在调用时间对父进程地址空间的一次复制,对于父进程fork() 返回child PID, 对于子进程返回值为0

代码实现

分配并初始化一个进程控制块

struct proc_struct {
enum proc_state state; // Process state
int pid; // Process ID
int runs; // the running times of Proces
uintptr_t kstack; // Process kernel stack
volatile bool need_resched; // bool value: need to be rescheduled to release CPU?
struct proc_struct *parent; // the parent process
struct mm_struct *mm; // Process's memory management field
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for current interrupt
uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT)
uint32_t flags; // Process flag
char name[PROC_NAME_LEN + 1]; // Process name
list_entry_t list_link; // Process link list
list_entry_t hash_link; // Process hash list
}; static struct proc_struct *alloc_proc(void) { struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
proc->state = PROC_UNINIT; //设置进程为未初始化状态
proc->pid = -1; //未初始化的的进程id为-1
proc->runs = 0; //初始化时间片
proc->kstack = 0; //内存栈的地址
proc->need_resched = 0; //是否需要调度设为不需要
proc->parent = NULL; //父节点设为空
proc->mm = NULL; //虚拟内存设为空
memset(&(proc->context), 0, sizeof(struct context));//上下文的初始化
proc->tf = NULL; //中断帧指针置为空
proc->cr3 = boot_cr3; //页目录设为内核页目录表的基址
proc->flags = 0; //标志位
memset(proc->name, 0, PROC_NAME_LEN);//进程名
}
return proc;
}

proc_struct即是进程控制块的结构体,alloc_proc函数来负责分配一个新的struct proc_struct结构,根据提示我们需要初始化一些变量

为新创建的内核线程分配资源

创建内核线程的主要工作由do_fork来完成资源的分配

int
do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
int ret = -E_NO_FREE_PROC;
struct proc_struct *proc;
if (nr_process >= MAX_PROCESS) {
goto fork_out;
}
ret = -E_NO_MEM;
//1:调用alloc_proc()函数申请内存块,如果失败,直接返回处理
if ((proc = alloc_proc()) == NULL) {
goto fork_out;
}
//2.将子进程的父节点设置为当前进程
proc->parent = current;
//3.调用setup_stack()函数为进程分配一个内核栈
if (setup_kstack(proc) != 0) {
goto bad_fork_cleanup_proc;
}
//4.调用copy_mm()函数复制父进程的内存信息到子进程
if (copy_mm(clone_flags, proc) != 0) {
goto bad_fork_cleanup_kstack;
}
//5.调用copy_thread()函数复制父进程的中断帧和上下文信息
copy_thread(proc, stack, tf);
//6.将新进程添加到进程的hash列表中
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc); //建立映射
nr_process ++; //进程数加1
list_add(&proc_list, &(proc->list_link));//将进程加入到进程的链表中
}
local_intr_restore(intr_flag);
// 7.一切就绪,唤醒子进程
wakeup_proc(proc);
// 8.返回子进程的pid
ret = proc->pid;
fork_out:
return ret; bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
  • 1.分配并初始化进程控制块(alloc_proc 函数);
  • 2.分配并初始化内核栈(setup_stack 函数);
  • 3.根据 clone_flag标志复制或共享进程内存管理结构(copy_mm 函数);
  • 4.设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文

    (copy_thread函数);
  • 5.把设置好的进程控制块放入hash_list 和proc_list 两个全局进程链表中;
  • 6.自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
  • 7.设置返回码为子进程的 id号。

proc_run完成进程切换

先看调度函数,就只是一个简单FIFO模型

void
schedule(void) {
bool intr_flag;
list_entry_t *le, *last;
struct proc_struct *next = NULL;
local_intr_save(intr_flag);
{
current->need_resched = 0;
last = (current == idleproc) ? &proc_list : &(current->list_link);
le = last;
do {
if ((le = list_next(le)) != &proc_list) {
next = le2proc(le, list_link);
if (next->state == PROC_RUNNABLE) {
break;
}
}
} while (le != last);
if (next == NULL || next->state != PROC_RUNNABLE) {
next = idleproc;
}
next->runs ++;
if (next != current) {
proc_run(next);
}
}
local_intr_restore(intr_flag);
}
  • 1、设置当前内核线程 current->need_resched 为 0;
  • 2、在 proc_list 队列中查找下一个处于就绪态的线程或进程 next;
  • 3、找到这样的进程后,就调用 proc_run 函数,保存当前进程 current 的执行现场(进程上下文),恢复新进程的执行现场,完成进程切换。

调度完成由proc_run来切换和开始执行进程

void proc_run(struct proc_struct *proc) {
if (proc != current) {
bool intr_flag;
struct proc_struct *prev = current, *next = proc;
local_intr_save(intr_flag);
{
current = proc;
load_esp0(next->kstack + KSTACKSIZE);
lcr3(next->cr3);
switch_to(&(prev->context), &(next->context));
}
local_intr_restore(intr_flag);
}
}
  • 1、让 current 指向 next 内核线程 initproc;
  • 2、设置任务状态段 ts 中特权态 0 下的栈顶指针 esp0 为 next 内核线程 initproc 的内核栈的栈顶,即 next->kstack + KSTACKSIZE ;
  • 3、设置 CR3 寄存器的值为 next 内核线程 initproc 的页目录表起始地址 next->cr3,这实际上是完成进程间的页表切换;
  • 4、由 switch_to函数完成具体的两个线程的执行现场切换,即切换各个寄存器,当 switch_to 函数执行完“ret”指令后,就切换到 initproc 执行了。
switch_to:                      # switch_to(from, to)
# save from's registers
movl 4(%esp), %eax # eax points to from
popl 0(%eax) # save eip !popl
movl %esp, 4(%eax)
movl %ebx, 8(%eax)
movl %ecx, 12(%eax)
movl %edx, 16(%eax)
movl %esi, 20(%eax)
movl %edi, 24(%eax)
movl %ebp, 28(%eax) # restore to's registers
movl 4(%esp), %eax # not 8(%esp): popped return address already
# eax now points to to
movl 28(%eax), %ebp
movl 24(%eax), %edi
movl 20(%eax), %esi
movl 16(%eax), %edx
movl 12(%eax), %ecx
movl 8(%eax), %ebx
movl 4(%eax), %esp
pushl 0(%eax) # push eip
ret

这些指令完成了保存前一个进程的其他 7 个寄存器到 context 中的相应域中。至此前一个进程的执行现场保存完毕。

再往后是恢复向一个进程的执行现场,这其实就是上述保存过程的逆执行过程,即从 context 的高地址的域 ebp 开始,逐一把相关域的值赋值给对应的寄存器。

加载应用程序并执行

完成应用程序的加载的函数是do_evecve,其中最主要的是load_icode,这个函数用来将ELF可执行二进制文件加载到当前内存中来

int
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
struct mm_struct *mm = current->mm;
if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
return -E_INVAL;
}
if (len > PROC_NAME_LEN) {
len = PROC_NAME_LEN;
} char local_name[PROC_NAME_LEN + 1];
memset(local_name, 0, sizeof(local_name));
memcpy(local_name, name, len); if (mm != NULL) {
lcr3(boot_cr3);
if (mm_count_dec(mm) == 0) {
exit_mmap(mm);
put_pgdir(mm);
mm_destroy(mm);
}
current->mm = NULL;
}
int ret;
if ((ret = load_icode(binary, size)) != 0) {
goto execve_exit;
}
set_proc_name(current, local_name);
return 0; execve_exit:
do_exit(ret);
panic("already exit: %e.\n", ret);
}

首先为加载新的执行码做好用户态内存空间清空准备。如果mm不为NULL,则设置页表

为内核空间页表,且进一步判断mm的引用计数减1后是否为0,如果为0,则表明没有进

程再需要此进程所占用的内存空间,为此将根据mm中的记录,释放进程所占用户空间内

存和进程页表本身所占空间。最后把当前进程的mm内存管理指针为空。由于此处的

initproc是内核线程,所以mm为NULL,整个处理都不会做

static int
load_icode(unsigned char *binary, size_t size) {
if (current->mm != NULL) {
panic("load_icode: current->mm must be empty.\n");
} int ret = -E_NO_MEM;
struct mm_struct *mm;
//(1) create a new mm for current process
if ((mm = mm_create()) == NULL) {
goto bad_mm;
}
//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
if (setup_pgdir(mm) != 0) {
goto bad_pgdir_cleanup_mm;
}
//(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
struct Page *page;
//(3.1) get the file header of the bianry program (ELF format)
struct elfhdr *elf = (struct elfhdr *)binary;
//(3.2) get the entry of the program section headers of the bianry program (ELF format)
struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);
//(3.3) This program is valid?
if (elf->e_magic != ELF_MAGIC) {
ret = -E_INVAL_ELF;
goto bad_elf_cleanup_pgdir;
} uint32_t vm_flags, perm;
struct proghdr *ph_end = ph + elf->e_phnum;
for (; ph < ph_end; ph ++) {
//(3.4) find every program section headers
if (ph->p_type != ELF_PT_LOAD) {
continue ;
}
if (ph->p_filesz > ph->p_memsz) {
ret = -E_INVAL_ELF;
goto bad_cleanup_mmap;
}
if (ph->p_filesz == 0) {
continue ;
}
//(3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
vm_flags = 0, perm = PTE_U;
if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;
if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
if (vm_flags & VM_WRITE) perm |= PTE_W;
if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap;
}
unsigned char *from = binary + ph->p_offset;
size_t off, size;
uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE); ret = -E_NO_MEM; //(3.6) alloc memory, and copy the contents of every program section (from, from+end) to process's memory (la, la+end)
end = ph->p_va + ph->p_filesz;
//(3.6.1) copy TEXT/DATA section of bianry program
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memcpy(page2kva(page) + off, from, size);
start += size, from += size;
} //(3.6.2) build BSS section of binary program
end = ph->p_va + ph->p_memsz;
if (start < la) {
/* ph->p_memsz == ph->p_filesz */
if (start == end) {
continue ;
}
off = start + PGSIZE - la, size = PGSIZE - off;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
assert((end < la && start == end) || (end >= la && start == la));
}
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
}
}
//(4) build user stack memory
vm_flags = VM_READ | VM_WRITE | VM_STACK;
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap;
}
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL); //(5) set current process's mm, sr3, and set CR3 reg = physical addr of Page Directory
mm_count_inc(mm);
current->mm = mm;
current->cr3 = PADDR(mm->pgdir);
lcr3(PADDR(mm->pgdir)); //(6) setup trapframe for user environment
struct trapframe *tf = current->tf;
memset(tf, 0, sizeof(struct trapframe));
/* LAB5:EXERCISE1 YOUR CODE
* should set tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags
* NOTICE: If we set trapframe correctly, then the user level process can return to USER MODE from kernel. So
* tf_cs should be USER_CS segment (see memlayout.h)
* tf_ds=tf_es=tf_ss should be USER_DS segment
* tf_esp should be the top addr of user stack (USTACKTOP)
* tf_eip should be the entry point of this binary program (elf->e_entry)
* tf_eflags should be set to enable computer to produce Interrupt
*/
tf->tf_cs = USER_CS;
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = USTACKTOP;
tf->tf_eip = elf->e_entry;
tf->tf_eflags = FL_IF;
ret = 0;
out:
return ret;
bad_cleanup_mmap:
exit_mmap(mm);
bad_elf_cleanup_pgdir:
put_pgdir(mm);
bad_pgdir_cleanup_mm:
mm_destroy(mm);
bad_mm:
goto out;
}
  • 调用mm_create函数来申请进程的内存管理数据结构mm所需内存空间,并对mm进行初

    始化;

  • 调用setup_pgdir来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核

    虚空间映射的内核页表(boot_pgdir所指) 的内容拷贝到此新目录表中,最后让mm->pgdir指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核虚空间;

  • 根据应用程序执行码的起始位置来解析此ELF格式的执行程序,并调用mm_map函数根

    据ELF格式的执行程序说明的各个段(代码段、数据段、BSS段等) 的起始位置和大小建

    立对应的vma结构,并把vma插入到mm结构中,从而表明了用户进程的合法用户态虚拟

    地址空间;

  • 调用根据执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置

    确定虚拟地址,并在页表中建立好物理地址和虚拟地址的映射关系,然后把执行程序各

    个段的内容拷贝到相应的内核虚拟地址中,至此应用程序执行码和数据已经根据编译时

    设定地址放置到虚拟内存中了

  • 需要给用户进程设置用户栈,为此调用mm_mmap函数建立用户栈的vma结构,明确用户

    栈的位置在用户虚空间的顶端,大小为256个页,即1MB,并分配一定数量的物理内存且

    建立好栈的虚地址<-->物理地址映射关系

  • 至此,进程内的内存管理vma和mm数据结构已经建立完成,于是把mm->pgdir赋值到cr3

    寄存器中,即更新了用户进程的虚拟内存空间,此时的initproc已经被hello的代码和数据

    覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没建立好

  • 先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令“iret”后,能

    够让CPU转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和

    堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断

Lab4\5:进程和线程的更多相关文章

  1. 关于Java中进程和线程的详解

    一.进程:是程序的一次动态执行,它对应着从代码加载,执行至执行完毕的一个完整的过程,是一个动态的实体,它有自己的生命 周期.它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而 ...

  2. Java中的进程和线程

     Java中的进程与线程 一:进程与线程 概述:几乎任何的操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程.当一个进程运行时,内部可能包括多个顺序执行流,每个顺序执行流就是 ...

  3. Java中的进程与线程(总结篇)

    详细文档: Java中的进程与线程.rar 474KB 1/7/2017 6:21:15 PM 概述: 几乎任何的操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程.当一个进 ...

  4. C# - 多线程 之 进程与线程

    并行~并发 并发 Concurrency,逻辑上的同时发生,一个处理器(在不同时刻或者说在同一时间间隔内)"同时"处理多个任务.宏观上是并发的,微观上是按排队等待.唤醒.执行的步骤 ...

  5. Android 的进程和线程

    进程和线程 如果某个应用程序组件是第一次被启动,且这时应用程序也没有其他组件在运行,则android系统会为应用程序创建一个包含单个线程的linux进程.默认情况下,同一个应用程序的所有组件都运行在同 ...

  6. Python之路第一课Day9--随堂笔记之二(进程、线程、协程篇)

    本节内容 进程.与线程区别 python GIL全局解释器锁 线程 语法 join 线程锁之Lock\Rlock\信号量 将线程变为守护进程 Event事件 queue队列 生产者消费者模型 Queu ...

  7. python之进程与线程

    什么是操作系统       可能很多人都会说,我们平时装的windows7 windows10都是操作系统,没错,他们都是操作系统.还有没有其他的? 想想我们使用的手机,Google公司的Androi ...

  8. [转载]了解Linux的进程与线程

    本文转自Tim Yang的博客http://timyang.net/linux/linux-process/ .对于理解Linux的进程与线程非常有帮助.支持原创.尊重原创,分享知识! 上周碰到部署在 ...

  9. python 进程和线程

    python中的进程.线程(threading.multiprocessing.Queue.subprocess) Python中的进程与线程 学习知识,我们不但要知其然,还是知其所以然.你做到了你就 ...

  10. Python_Day10_进程、线程、协程

    本节内容    操作系统发展史介绍    进程.与线程区别    python GIL全局解释器锁    线程        语法        join        线程锁之Lock\Rlock\ ...

随机推荐

  1. Redis(二)数据结构与键管理

    一.基础知识 1.全局命令 keys *   :查看所有键 dbsize:返回当前数据库中键的总数 exists key:检查键是否存在 del key ... :删除键 expire key sec ...

  2. 小程序多端差异调研报告(微信,支付宝,头条,QQ)

    已经使用uni-app开发并发布了一个跨端小程序啦,嘻嘻嘻!

  3. git的使用和常用命令

    git介绍 git 是一个免费开源的分布式版本控制系统 git可以实现各个版本之间的来回穿梭 git可以远程托管代码 git可以完成团队合作 workspace --add--> index - ...

  4. C语言I博客作业06

    这个作业属于哪个课程 C语言程序设计I 这个作业要求在哪里 作业链接 我在这个课程的目标是 熟悉分支结构 这个作业在那个具体方面帮助我实现目标 可以更完整的编写程序及博客园 参考文献 [参考文献](h ...

  5. linux sqlite安装

    wget http://www.sqlite.org/sqlite-3.6.16.tar.gz tar -zxvf sqlite-3.6.16.tar.gz  cd sqlite-3.6.16 ./c ...

  6. Linux nfs服务介绍

    nfs服务介绍 nfs(Network File System) 网络文件系统,能使用户访问服务器的文件系统,就像访问自己的本机的文件系统一样,并且多个 客户端共享访问该文件系统. 目前nfs服务,较 ...

  7. 在VMware下通过挂载系统光盘搭建本地yum仓库的方法

    一.虚拟机的安装 首先你要有一个VMware虚拟机,没有软件的朋友可以看我的前几篇博客 安装VMware虚拟机 二.进入虚拟机(在这里我们进入一个Linux虚拟机下的CentOS操作系统进行演示) 首 ...

  8. C语言I—2019秋作业01

    1您对软件工程专业或计算机科学与技术专业了解是什么? 工程专业将成为一个新的热门专业.软件工程专业以计算机科学与技术学科为基础,突出软件开发的工程性,使学生在掌握计算机科学与技术方面知识和技能的基础上 ...

  9. 「刷题」GERALD07加强版

    是LCT了. 首先我们不知道联通块怎么数. 然后颓标签知道了是LCT. 那么考虑一下怎么LCT搞. 有一个很普遍的思路大家也应该都知道,就是如何求一个区间中某种颜色的个数. 这个可以很简单的用主席树来 ...

  10. DAY 4 基础算法

    基础算法 本来今天是要讲枚举暴力还有什么的,没想到老师就说句那种题目就猪国杀,还说只是难打,不是难.... STL(一)set 感觉今天讲了好多,set,单调栈,单调队列,单调栈和单调队列保证了序列的 ...