基于DOS的多任务系统的实现
2015-11-06 17:31
417 查看
一, 课程设计的目的
通过对线程(和进程)的创建和撤销,CPU的调度,同步机制,通信机制的实现,以达到一下目的:1, 加深对线程和进程概念的理解,明确进程和程序的区别。
2, 加深对CPU调度过程(现场保护,CPU的分派和现场的恢复)的理解。
3, 进一步认识并执行的概念,明确顺序执行和并发执行的区别。
4, 加深对临界资源,临界区,信号量以及同步机制的理解。
5, 加深对消息缓冲通信的理解。
二, 设计要求
1, 用C语言完成线程的创建和撤销,并按优先权加时间片轮转算法对多线程进行调度。2, 改变时间片的大小,观察结果的变化,
3, 假设两个线程共用同一软件资源(如某以变量,或者某以数据结构),请用记录型信号量来实现对它的互斥访问。
4, 假设有两个线程共享一个可以存放5个整数的缓冲,一线程不停地计算1至50的平方,并将结构放入缓冲中,另一个线程不断地从缓冲中取出结果,并将它们打印出来,请用记录型信号量实现这一生产者和消费者的同步问题。
5, 实现消息缓冲通信,并与3,4中的简单通信进行比较。
三, 程序设计思想以及总流程图
1, 程序的设计思想:
该程序主要是分5大块内容:线程的创建和撤销,线程的调度,线程的同步与互斥,线程的阻塞与唤醒,利用消息缓冲队列的线程间的通信。由这五大块功能来完成的基于DOS的多任务系统的实现。在这个系统中,首先先由main函数进行一些初始化工作,然后直接创建0#线程对应于main函数,再由0#线程调用create创建1#,2#线程分别对应与函数f1(),f2(),最后将系统的中断服务程序设置为new_int8,并把控制交给1#线程,启动多个线程的并发执行。0#线程是一个比较特殊的线程,它在创建的时候没有使用create来创建,而是在系统初始化后直接创建的,因它对应的程序段为main函数中的一段,所以也直接使用整个系统的堆栈,而不再创建时为私有堆栈分配额外的空间;同样,撤销的时也不需要释放私有堆栈的空间,所以也没有over()函数而是直接撤销,从这方面来看,它是一个系统线程。
此外,在启动多个线程并发执行过程后,0#线程将系统控制权转交出去,直至系统中其他进程都不具备执行条件时,它才有可能重新得到CPU,从这方面看,0#线程相当于是一个空转线程,最后,0#线程还担负着一个特别的使命:等待系统中所有其他的线程的完成,此时,它将直接撤销自己并恢复原来的时钟中断服务程序,从此终止整个多任务系统。
2, 系统的总流程图
四, 系统各个功能的实现思想
1, 线程的创建和撤销
线程的创建过程关键就是对私有堆栈和TCB初始化的过程,其过程如下:i, 为新线程分配一空闲的线程控制块
ii, 为新线程的私有堆栈分配内存空间(因为对等线程共享程序段和数据段空间,所以创建线程时不必像创建进程那样再为程序段和数据段分配内存空间)
iii, 初始化新线程的私有堆栈,即按CPU调度时现场信息的保存格式布置堆栈。
iv, 初始化线程控制块,即填入线程的外部标识符,设置好线程私有堆栈的始址,段址和栈顶指针,将线程的状态置为就绪状态。
v, 最后哦返回新线程的内部标识符
vi, 线程的内存映像如下:
线程的撤销过程中,一个关键的地方是在初始化线程私有堆栈时 需要将over()的入口地址压入线程的私有堆栈中,这样做的好处是:当线程所对应的函数正常结束时,over()函数的入口地址将最为函数的返回地址被弹出至CS,IP寄存器,那么控制将自动转向over(),从而使对应的线程被自动撤销,并重新进行CPU调度。
2, 线程的调度
引起CPU调度原因主要是有三种情况:时间片到时,线程执行完毕或正在执行的线程因等待某种事件而不能继续执行。由这些原因,调度程序可以通过两个函数分别处理不同原因引起的调度:
New_int8()函数主要是处理因时间片到时引起的调度该调度可以通过截取时钟中断(int 08)来完成;
Swtch()函数主要是处理因其他原因引起的调度;
New_int8()函数因为是通过截取时钟中断来实现,可以知道其是属于系统调度,由于涉及到系统调度的函数 都是需要对DOS状态进行判断,以防止出现系统数据混乱等情况的发生(从Dos的不可重入性来得出),而Swtch()函数是处理因其他原因引起的调度,所以它所涉及到的仅仅是用户级的函数调度,没有涉及到系统级的函数调度,因此Swtch()函数不需要对Dos状态进行判断。对于线程的两种调度函数的过程,因其相似,给出New_int8()函数的执行过程图,如下:
需要主要的是:新的时钟中断处理程序不能太长,否则系统效率将大大下降甚至使系统无法正常工作;在新的时钟中断处理程序必须调用系统原来的INT 08H,否则将影响磁盘马达的关闭和系统的计时,另外,我们还主要依赖原来的INT 08H向中断控制器发中断结束指令(EOI);
3, 线程的阻塞与唤醒
线程的阻塞:主要是当某一线程需要阻塞的时候,将其插入阻塞队列中,等待唤醒进程唤醒,所以其过程为:首先,将线程的状态置为阻塞态,然后将线程插入指定的阻塞队列末尾,并重新进行CPU调度。线程的唤醒:主要是唤醒阻塞队列里面的线程,所以其过程是:把阻塞队列头上的第一个线程的TCB取下来,并将其状态改为就绪状态,等待CPU调度
4, 线程的同步与互斥
在这个系统中是采用记录型信号量机制来实现同步与互斥的,实现的方法:采用P ,V操作,设置两个信号量:一个为互斥信号量,一个为临界资源数目;
5, 利用消息缓冲队列的线程间通信
线程间的通信,关键采用send()与receive()来实现,通过发送一个文本信息来显示通信的过程,其过程为:send()函数:消息的发送者需要提供接收者的标识符,消息的长度以及消息正文的起始地址等信息,然后在发送原语里申请一空闲的消息缓冲区,用相应的信息来装配该消息缓冲区,并把它插入到接收者的消息队列中去。
Receive()函数:消息的接受者必须给出发送者的标识符,接受区的起始地址等信息,然后从自己的消息队列中取得相应的发送者发送来的消息缓冲区,将消息正文复制到接受区中,并释放相应的消息缓冲区。
五、详细程序设计
#include <stdlib.h> #include <stdio.h> #include <dos.h> /***********************定义***********************/ #define GET_INDOS 0x34 #define GET_CRIT_ERR 0x5d06 /*定义四个状态*/ #define finished 0 #define running 1 #define ready 2 #define blocked 3 #define TL 3 /*设置TL(时间片)时间为3*/ #define NTCB 10 /*NTCB是系统允许的最多任务数也就是进程数*/ #define NBUF 5 #define NTEXT 30 /**********************声明变量********************/ char far *indos_ptr=0; char far *crit_err_ptr=0; int current; /*全部变量,始终等于正在执行的线程的内部标识符*/ int timecount=0; /*全局变量,等于上次调度至今的时间,在每次时钟中断发生时,timecount+1,通过它与TL课判断时间片是否到时,从而决定是否进行CPU调度*/ /********************定义数据结构********************/ typedef int (far *codeptr)(void);/*定义codeptr函数指针*/ /*定义记录型信号量的数据结构*/ typedef struct { int value; struct TCB *wq; }semaphore; semaphore mutexfb={1,NULL}; /*互斥信号量*/ semaphore sfb={NBUF,NULL}; /*空闲缓冲队列的计数信号量*/ /*消息缓冲区的数据结构*/ struct buffer { int sender; /*消息发送者的标识数*/ int size; /*消息长度<=NTEXT个字节*/ char text[NTEXT]; /*消息正文*/ struct buffer *next; /指向下一个消息缓冲区的指针*/ }; struct buffer *freebuf; /*空闲消息缓冲队列,是临界资源,由NBUF个空闲的消息缓冲区组成*/ /*定义TCB数据结构*/ struct TCB{ unsigned char *stack; /*堆栈的起始地址*/ unsigned ss; /*堆栈的段址*/ unsigned sp; /*堆栈的栈指针*/ char state; /*线程的状态*/ char name[10]; /*线程的外部标示符*/ struct TCB* next; /*链接字段,把所有就绪的线程按某种方式排成一显式队列,如优先权从高到底的队列*/ struct buffer *mq; /*消息队列队首指针*/ semaphore mutex; /*消息队列的互斥信号量*/ semaphore sm; /*消息队列计数信号量*/ int value; } tcb[NTCB]; /*NTCB是系统允许的最多任务数*/ /*现场保护和恢复中要用到的一个数据结构*/ struct int_regs{ unsigned bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,flags,off,seg; }; /**************************声明函数*************************/ int DosBusy(void); void InitInDos(void); void InitTcb(void); /*对TCB的初始化*/ int create(char *name,codeptr code,int stacklen); void over(void); /*撤销线程,归还所占资源*/ void interrupt(*old_int8)(void); /*原来的时间中断程序,需要先声明*/ void interrupt new_int8(void); /*因时间片到时而引起的调度由new_int8()函数来完成*/ void interrupt swtch(void); /*其他原因引起的CPU调度由函数swtch()完成*/ void tcb_state(void); /*输出所有线程的状态信息*/ int all_finished(void); void p(semaphore *sem); /*信号量P操作*/ void v(semaphore *sem); /*信号量V操作*/ /*********************函数的实现*********************/ /*******InitInDos函数的实现********/ void InitInDos(void){ union REGS regs; struct SREGS segregs; /*获得INDOS flag 的地址*/ regs.h.ah=GET_INDOS; intdosx(®s,®s,&segregs), indos_ptr=MK_FP(segregs.es,regs.x.bx); /*get the address of CRIT_ERR flag*/ if(_osmajor<3) crit_err_ptr=indos_ptr+1; else if(_osmajor==3 && _osminor==0) crit_err_ptr=indos_ptr-1; else{ regs.x.ax=GET_CRIT_ERR, intdosx(®s,®s,&segregs); crit_err_ptr=MK_FP(segregs.ds,regs.x.si); } } /*************DosBusy函数的实现************/ int DosBusy(void){ if(indos_ptr&&crit_err_ptr) return (*indos_ptr|| *crit_err_ptr); else return (-1); } /************InitTcb函数的实现*************/ /*对TCB进行初始化*/ void InitTcb(void){ int i; for(i=1;i<NTCB;i++){ tcb[i].stack=NULL; tcb[i].state=finished; strcpy(tcb[i].name,'\0'); tcb[i].mq=NULL; tcb[i].sm.value=0; /*消息队列计数信号量*/ tcb[i].mutex.value=1; /*缓冲区的互斥信号量*/ } } /*************create函数的实现****************/ /*创建一对应于函数name(外部标识符)的线程*/ int create(char *name,codeptr code, int stacklen){ int i; char *p; struct int_regs *pt; /*第一步:寻找空白的TCB*/ for(i=1;i<NTCB;i++){ if(tcb[i].state==finished) break; } /*第二步:申请线程的私有堆栈内存空间,分配stacklen个字节长度的内存空间,利用malloc函数返回内存地址指针指向该内存空间, 所返回的值是该内存空间的起始地址*/ p=(char *)malloc(stacklen*sizeof(char)); /*获得堆栈的内存空间的高地址指针*/ p=p+stacklen; /*对地址进行类型转换*/ pt=(struct int_regs*)p; pt--; /*第三步:对线程的私有堆栈进行初始化;用FP_SEG和FP_OFF分别获得栈顶指针和栈顶指针的偏移量,此外系统对线程撤销的工作, 需要在该步骤完成:通过在堆栈初始化时 原先将over()函数的入口地址压入线程的私有堆栈中;那么当线程所对应的函数正常结束时, over()函数的入口地址将作为函数的返回地址被弹出到CS,IP寄存器,控制自动转向over()函数执行*/ pt->flags=0x200; /*flags寄存器的允许中断位*/ pt->cs=FP_SEG(code); /*代码段的段地址*/ pt->ip=FP_OFF(code); /*代码段的段内偏移地址*/ pt->ds=_DS; /*数据段的段地址*/ pt->es=_ES; /*附加数据段的段地址*/ pt->off=FP_OFF(over); /*撤销线程代码的偏移地址*/ pt->seg=FP_SEG(over); /*撤销线程代码的段址*/ /*第四步:初始化线程的控制块TCB*/ strcpy(tcb[i].name,name); /*填入线程的外部标识符*/ tcb[i].state=ready; /*将线程的状态置成就绪态*/ tcb[i].stack=p-stacklen; /*私有堆栈的起始地址*/ tcb[i].ss=FP_SEG(pt); /*当前线程的段地址*/ tcb[i].sp=FP_OFF(pt); /*当前线程的栈顶指针*/ return i; /*返回线程的内部标示符*/ } /************new_int8函数的实现***************/ /*系统调度,即时间中断到达后,判断时间片到后才运行,调用老的时钟中断*/ void interrupt new_int8(void){ int i; (*old_int8)(); /*调用原来的时钟中断服务程序*/ timecount++; /*每次发生中断时加1*/ if(timecount>=TL){ /*时间片到时*/ if(DosBusy()) /*如果Dos忙*/ return; disable(); /*关中*/ /*保护正在执行的线程current的现场,暂停它的执行*/ tcb[current].ss=_SS; tcb[current].sp=_SP; if(tcb[current].state==running) /*将执行状态变为就绪状态,暂停执行*/ tcb[current].state=ready; /*找到以新的就绪线程*/ for(i=1;i<NTCB;i++){ if(tcb[i].state==ready && i!=current) /*找到除了当前线程的其他就绪线程*/ break; } /*如果没有找到就绪线程,那么就回复当前线程,继续执行*/ if(i>=NTCB){ if(tcb[current].state==ready) tcb[current].state=running; enable(); return; /*如果超出了NTCB则恢复现场然后返回*/ } /*如果找到就绪线程,那么恢复线程i的现场,把CPU分配给它*/ _SS=tcb[i].ss; _SP=tcb[i].sp; tcb[i].state=running; /*置线程i为现有线程,并且重新开始计时*/ current=i; timecount=0; enable(); /*开中*/ } return; } /*********swtch函数的实现************/ /*针对Swtch()函数的实现:由于它是解决由其他因素所引起的CPU调度,在这个实现过程,只需要判断线程的执行状态即可,其他阻塞等状态不需要进行判断,或者可以直接对当前线程的现场进行保护,然后寻找就绪线程,分配CPU以及现场进行执行*/ /*Find()函数是为了寻找就绪线程而且是优先权大的线程(根据优先数越大,优先权越小的思想,在TCB设置以优先数,然后进行选择)*/ int Find() { int i,j; for(i=0;i<NTCB;i++) if(tcb[i].state==ready&&i!=current) break; if(i==NTCB) return -1; for(j=i+1;j<NTCB;j++) { if(tcb[j].state==ready&&j!=current) if(tcb[j].value>tcb[i].value) i=j; } return i; } /*swtch()调度,手工调度才能运行,处理因其他因素引起的中断*/ void interrupt swtch(void) { int i; i=Find(); if(i<0) i=0; disable(); tcb[current].ss=_SS; tcb[current].sp=_SP; if(tcb[current].state==running) tcb[current].state=ready; _SS=tcb[i].ss; _SP=tcb[i].sp; tcb[i].state=running; current=i; enable(); } /****************线程的阻塞和唤醒的实现****************/ /**(阻塞)block函数的实现**/ void block(struct TCB **qp){ struct TCB *tp; disable(); tp=*qp; tcb[current].state=blocked; /*首先要将当前线程的状态置为阻塞状态*/ /*需要将线程插入到指定的阻塞队列未尾,并重新进行CPU调度*/ (*qp)->next=NULL; if(tp==NULL) tp=&tcb[current]; /*由于tp是一个指针,所以操作的是指针*/ else{ while(tp->next!=NULL) tp=tp->next; tp->next=&tcb[current]; /*将阻塞线程插入到队尾*/ } enable(); swtch(); /*并重新进行CPU调度*/ } /**(唤醒)wakeup_first函数的实现**/ void wakeup_first(struct TCB **qp){ int i; struct TCB *tp; disable(); tp=*qp; /*寻找阻塞线程,因为线程状态的改变需要在TCB中修改,所以需要知道阻塞队列里面需要唤醒的线程对应TCB数组里面的哪一个*/ for(i=1;i<NTCB;i++){ if(strcmp(tcb[i].name,(*tp->next).name)==0){ /*如果两个外部标示符一样 说明找到需要唤醒的线程*/ break; } tcb[i].state=ready; /*将其状态改为就绪状态*/ enable(); } } /***************线程的同步和互斥的实现****************/ /*用记录型信号量机制实现同步与互斥*/ /**对信号量的P操作**/ void p(semaphore *sem){ struct TCB **qp; /*设置一个指向TCB链表的二级指针*/ disable(); /*关中断*/ sem->value=sem->value-1; /*记录型信号量的value值减1*/ if(sem->value<0){ /*如果记录型信号量的值小于0*/ qp=&(sem->wq); /*那么将qp指针指向sem信号量的阻塞队列*/ block(qp); /*阻塞相应进程到阻塞队列*/ } enable(); } /**对信号量的V操作**/ void v(semaphore *sem){ struct TCB **qp; disable(); qp=&(sem->wq); sem->value=sem->value+1; if(sem->value<=0){ wakeup_first(qp); } enable(); } /***************消息缓冲队列的线程间的通信*************/ /**初始化消息队列**/ void InitBuf(void){ int i; struct buffer *freebuf,*temp; freebuf=(struct buffer*)malloc(sizeof(struct buffer)); /*申请空间*/ temp=freebuf; for(i=1;i<=NBUF;i++){ temp=(struct buffer*)malloc(sizeof(struct buffer)); temp=temp->next; } } /**从空闲消息缓冲队列头上取下一缓冲区,返回指向该缓冲区的指针**/ struct buffer *getbuf(void){ struct buffer *buff; buff=freebuf; /*空闲消息缓冲头*/ freebuf=freebuf->next; return buff; } /**将buff所指的缓冲区插到*mq所指的缓冲队列尾**/ void insert (struct buffer **mq,struct buffer *buff){ struct buffer *temp; if(buff==NULL) return; buff->next=NULL; if(*mq==NULL) *mq=buff; else{ temp=*mq; while(temp->next!=NULL) temp=temp->next; temp->next=buff; } } /***将地址a开始的size个字节发送给外部标示符为receiver的线程***/ void send(char *receiver,char *a,int size){ struct buffer *buff; int i,id=-1; disable(); /*原语要关中断*/ /*首先需要进行搜索接受进程*/ for(i=0;i<NTCB;i++){ if(strcmp(receiver,tcb[i].name)==0){ id=i; break; } } /*如果没有收到,那么就显示错误,没有接收进程*/ if(id==-1){ printf("Error:Receiver not exist!\n"); enable(); return ; } printf("\n%s send %s a message:",tcb[current].name,receiver); printf("%s\n",a); p(&sfb); /*sfb为空闲缓冲区队列的计数信号量,为全局变量*/ p(&mutexfb); /*mutexfb为互斥信号量*/ buff=getbuf(); /*取一缓冲区*/ v(&mutexfb); /*释放互斥信号量,表示用完缓冲区*/ buff->sender=current; /*将发送方的内容加入缓冲区*/ buff->size=size; buff->next=NULL; for(i=0;i<buff->size;i++,a++) /*取得消息正文*/ buff->text[i]=*a; p(&tcb[id].mutex); /*互斥使用接收者线程的消息队列*/ insert(&(tcb[id].mq),buff); /*将消息缓冲区插入消息队列*/ v(&tcb[id].mutex); /*撤销线程id消息队列互斥信号,接收者线程多了个消息*/ v(&tcb[id].sm); /*消息队列计数信号量加1*/ enable(); } /*****释放缓冲区函数*****/ struct buffer *payback(struct buffer *bq){ struct buffer *temp; temp=freebuf; while(temp->next!=NULL) temp=temp->next; temp->next=bq; return freebuf; } /****接收者函数receiver****/ void receive(char *sender){ int i,j,id=-1; struct buffer *buff; disable(); for(i=1;i<NTCB;i++){ /*搜索发送进程的ID*/ if(strcmp(sender,tcb[i].name)==0){ id=i; break; } } if(id==-1){ /*如果发送线程已经终止*/ printf("Error:Sender not exist!\n"); tcb[current].state=blocked; tcb[current].ss=_SS; tcb[current].sp=_SP; for(j=1;j<NTCB;j++) /*寻找新的线程*/ { if(tcb[i].state==ready){ /*恢复新线程的现场*/ _SS=tcb[i].ss; _SP=tcb[i].sp; tcb[i].state=running; } } enable(); return; } p(&tcb[current].sm); /*当前进程消息队-1,取第一个缓冲区*/ p(&tcb[current].mutex); /*当前线程消息队列的互斥信号量*/ buff=tcb[current].mq; /*消息队列的首指针*/ tcb[current].mq=tcb[current].mq->next; /*下移一位*/ v(&tcb[current].mutex); /*释放当前线程消息队列的互斥信号*/ /*将消息缓冲区中信息复制到接受区*/ printf("\n%s receiver a message:",tcb[current].name); for(i=0;i<buff->size;i++) printf("%c",buff->text[i]); /*将消息正文复制出去*/ printf("\n"); p(&mutexfb); /*缓冲区信号量*/ /*释放缓冲区*/ buff->sender=-1; buff->size=0; strcpy(buff->text,'\0'); buff->next=NULL; /*将当前进程的消息缓冲区内的第一个消息删除,并且将缓冲量收回缓冲区*/ payback(buff); /*释放缓冲区信号量*/ v(&mutexfb); /*空闲缓冲区数+1*/ v(&sfb); printf("f1 and f2 is finished!"); enable(); } /************over函数的实现************/ /*撤销线程,收回被撤销线程的资源*/ void over(void){ disable();/*通过disable()与enable()来实现在执行该代码段时防止中断*/ /*撤销当前线程所申请的TCB内存空空间,因为一个线程在它执行完毕之后就需要撤销自己,所以是要用到current(当前)线程*/ free(tcb[current].stack); /*堆栈指针的释放*/ strcpy(tcb[current].name,'\0'); /*将线程的外部标示符置空*/ tcb[current].state=finished; /*将状态置为终止态*/ swtch(); /*在线程撤销后,需要重新进行CPU调度*/ enable(); } /************tcb_state函数的实现**************/ /*输出所有线程的状态*/ void tcb_state(void){ int i; for(i=1;i<NTCB;i++){ switch(tcb[i].state){ case 0: printf("The state of tcb[%d](%s) is finished\n",i,tcb[i].name); break; case 1: printf("The state of tcb[%d](%s) is running\n",i,tcb[i].name); break; case 2: printf("The state of tcb[%d](%s) is ready\n",i,tcb[i].name); break; case 3: printf("The state of tcb[%d](%s) is blocked\n",i,tcb[i].name); break; } } } /**************f1函数的实现****************/ void f1 (void){ int i,j,k; char c[NTEXT]="you received it,f2?"; for(i=0;i<10;i++){ putchar('a'); for(j=0;j<100000;j++){ for(k=0;k<5000;k++); } } send("f2",c,20); printf("f2 is waken up!\n"); } /***************f2函数的实现**************/ void f2(void){ long i,j,k; for(i=0;i<10;i++){ putchar('b'); for(j=0;j<100000;j++) for(k=0;k<5000;k++); } receive("f1"); } /*************all_finished函数的实现*****************/ /*判断是否全部线程都已经完成*/ int all_finished(void){ int i; for(i=1;i<NTCB;i++){ if(tcb[i].state!=finished) return 0; } return 1; } /****************************主函数*******************************/ void main() { InitInDos(); InitTcb(); old_int8=getvect(8); /*在使用新的时钟中断服务程序前,必须先用getvect()函数获取系统原来的INT 08H的入口地址并将它保存起来*/ /*创建0#线程*/ strcpy(tcb[0].name,"main"); tcb[0].state=running; current=0; /*创建f1,f2线程后,在CPU中,CS,IP指向current=0的函数,而ss,sp指向main函数*/ create("f1",(codeptr)f1,1024); create("f2",(codeptr)f2,1024); tcb_state(); /*启动多个线程的并发执行*/ setvect(8,new_int8); swtch(); while(!all_finished()){} tcb[0].name[0]='\0'; tcb[0].state=finished; setvect(8,old_int8); /*回复8号中断*/ tcb_state(); printf("\n Multi_task system terminated. \n"); }
相关文章推荐
- hdu 1867 A + B for you again
- 一台机器安装多个MySQL
- IOC容器——Unity
- WS+MQ+WCF+EF(Code First)
- C++获取数字证书的序列号
- centos发送邮件
- haproxy+heartbeat的两种方案(注意,备服务器vip,正常haproxy启动方法)
- 类 Mat导言
- libevent代码阅读(11)——“hello-world.c”之 进入事件多路分发循环
- 1097. Deduplication on a Linked List
- MongoDB的用户管理(6)
- c#在一般处理程序中使用session
- mysql数据库设置不区分大小写
- 初入Django(一)--创建第一个项目
- 二个数调换位置
- Windows如何修改MySQL用户root密码
- 自学php-数据基础
- 事物TransactionScope
- 可视化数据分析(二) 一个简单的柱状图的实现
- ZooKeeper的事务日志和快照