这是学习哈工大李治军在mooc课操作系统时做的实验记录。原实验报告在实验楼上。现转移到这里。备以后整理之用。

完整的实验代码见:实验楼代码

一、tss方式的进程切换

Linux0.11中默认使用的是硬件支持的tss切换,系统为每个进程分配一个tss结构用来存储进程的运行信息(上下文环境),然后通过CPU的一个长跳转指令ljmp来实现进程的切换,这种方式易于实现,但一者不便于管理多CPU进程,二者效率不佳,故此次实验要将系统中使用的tss切换方式修改为栈切换方式。而由于CPU管理方式的原因,tr寄存器必须指向当前正在运行的进程的tss结构,所以系统在运行时必须要有一个tss结构来存储当前运行的进程(线程)的上下文信息,但只需要保留一个全局的tss结构即可(由于这里不考虑多CPU情况,如果考虑则要为每一个CPU准备一个tss结构来储存每个cpu运行时的进程或线程上下文信息)。

从tss结构(见下面)中可以看出,tss结构基本上就是一个全部运行时寄存器保存结构,就是用来保存进程运行时全部的寄存器信息的。当然,也有其他一些进程相关信息,如进程分组链接back_link,IO使用信息trace_bitmap,以及协处理器i387的信息。当进程进行切换时这些信息通过ljmp语句进行交换,被调度到的进程信息进入相应寄存器,而被调度出的进程则使用这个结构保存进程被调度时的寄存器状态及数据,以备下次运行时恢复。而这一切均通过ljmp语句进行。见switch_to宏的代码(如下)。从代码中可以看到这种进程的切换方式简洁方便,但其缺乏效率,而且也不能很好地向多cpu结构扩展。所以要修改为基于栈的切换方式。

struct tss_struct {
long back_link; /* 16 high bits zero */
long esp0;
long ss0; /* 16 high bits zero */
long esp1;
long ss1; /* 16 high bits zero */
long esp2;
long ss2; /* 16 high bits zero */
long cr3;
long eip;
long eflags;
long eax,ecx,edx,ebx;
long esp;
long ebp;
long esi;
long edi;
long es; /* 16 high bits zero */
long cs; /* 16 high bits zero */
long ss; /* 16 high bits zero */
long ds; /* 16 high bits zero */
long fs; /* 16 high bits zero */
long gs; /* 16 high bits zero */
long ldt; /* 16 high bits zero */
long trace_bitmap; /* bits: trace 0, bitmap 16-31 */
struct i387_struct i387;
};
\\宏switch_to(next):

#define switch_tss(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \  //比较当前要切换的进程是否是当前运行的进程
"je 1f\n\t" \  //如果是则不调度直接退出
"movw %%dx,%1\n\t" \  //将要调度的tss指针存到tmp.b中
"xchgl %%ecx,current\n\t" \ //交换pcb值,要调度的存储在ecx中
"ljmp *%0\n\t" \ //进行长跳转,即切换tss
"cmpl %%ecx,last_task_used_math\n\t" \ //进行切换后的处理协处理器
"jne 1f\n\t" \
"clts\n" \
"1:" \
::"m" (*&__tmp.a),"m" (*&__tmp.b), \
"d" (_TSS(n)),"c" ((long) task[n])); \
}

二、基于内核栈的切换

2.1、关于切换的分析

不管使用何种方式进行进程切换(此次实验不涉及线程),总之要实现调度进程的寄存器的保存和切换,也就是说只要有办法保存被调度出cpu的进程的寄存器状态及数据,再把调度的进程的寄存器状态及数据放入到cpu的相应寄存器中即可完成进程的切换。由于切换都是在内核态下完成的所以两个进程之间的tss结构中只有几个信息是不同的,其中esp和trace_bitmap是必须切换的,但在0.11的系统中,所有进程的bitmap均一样,所以也可以不用切换。

调度进程的切换方式修改之前,我们考虑一个问题,进程0不是通过调度运行的,那进程0的上下文是如何建立的?因为在进程0运行时系统中并没有其他进程,所以进程0的建立模板一定可以为进程栈切换方式有帮助。所以先来分析一下进程0的产生。进程0是在move_to_user_mode宏之后直接进入的。在这之前一些准备工做主要是task_struct结构的填充。

#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \
"pushl %%eax\n\t" \
"pushfl\n\t" \
"pushl $0x0f\n\t" \
"pushl $1f\n\t" \
"iret\n" \
"1:\tmovl $0x17,%%eax\n\t" \
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")

这个宏模拟了一个中断返回,把但却没有设置cs、ss,其原因应该是进程0代码和数据均在内核,所以不需要设置,这样就可以知道,在使用栈切换时我们也只要模仿这种方式,手动造一个中断返回即可。但实际上还要麻烦一点,因为进程0不通过schedule进行第一次运行,但我们fork出来的新进程却要经过schedule,也就是要经过switch_to来进行调度,所以在新fork的进程栈中不仅要模拟中断返回,还要为schedule的返回准备数据。基于此,新fork的进程的内核栈应该更类似于一个被调度出去的一个进程。经过分析代码我们可以知道一个两个进程在调度时内核的栈的状态如下:

\\                                  进程A的内核栈           进程B的内核栈
int前 ss ss
esp esp
eflags eflags
cs cs
eip eip
进入Int之后返回之前 ds ds
es es
fs fs
edx edx
ecs ecs
ebx ebx
eax eax
进入schedule的switch后 返回地址retur_from_systemcall 返回地址retur_from_systemcall

所以我们只要把fork的进程的内核栈的状态手工打造成类似的样子就可以了。根据实验指导的内容,此次修改将switch_to宏变成函数,所以需要压栈,故相比上图要做一定的调整,因为c函数的调用参数是通过栈来传递的,其汇编后的代码一般如下:

pushl ebp
movl esp,ebp
sub $x,esp

此时栈内会放置函数所用的参数,新进程并没有经过schedule的前一部分之直跳进来的,所以fork中的进程栈中也必须要有这些参数数据。此外由于新进程也没有经过int,所以iret返回时所需要的数据也要在栈内准备好。

2.2、基于栈的切换代码

本次要修改的文件如下:

1、schedule.h

这个文件中主要修改如下几处。 第一,task_struct结构的相关部分,由于之前使用tss切换,而此次修改要使用栈切换,tss结构弃之不用(只保留一个进程0的),所以esp指针必须要保存到task_struct中,因此要在task_struct结构中添加一个新的成员long kernelstack; //内核栈指针,实际上此次实验的最后结果中我添加了两个,另一个是long eip; //保存切换时使用的EIP指针,具体原因后面会说明。同时进程0的task_struct也要做相应修改。老师在视频中也讲到成员添加的位置要小心,因为有些成员在系统中被硬编码了,所以不能放在结构的最前面,也最好不要放最后面,因为那样,在汇编程序中计算新添加成员偏移位置时就变得困难了。

此外,为了能在函数中使用汇编中的函数,这里还要声明如下几个函数。

/*添加用来PCB切换的函数声明*/
extern void switch_to(struct task_struct * pnext,int next); //这里的参数类型是不重要的,记得C语言
//的老师说过编译器只并不对此处的类型进行严格检查。
extern void first_return_from_kernel(void);
extern void first_switch_from(void); //这里也是在实验指导之外添加的

2、schedule.c

要修改的位置就是schedule函数,因为要将switch_to宏修改为一个汇编函数,所以无法在里面使用宏在查找pcb指针以及ldt指针,所以这两个数据均要使用参数来传递,故在schedule中也添加了一个新成员用来保存当前要调度进cpu的进程的pcb指针。如下:

struct task_struct * pnext=NULL; //保存PCB指针,

while () {
c = -;
next = ;
/*为pnext赋初值,让其总有值可用。*/
pnext=task[next]; //最初我并没有加这句,导致如果系统没有进程可以调度时传递进去的是一个空值,
//系统宕机,所以加上这句,这样就可以在next=0时不会有空指针传递。
/**/
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i,pnext=*p; //保存要调度到的pcb指针
}
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> ) +
(*p)->priority;
}
/*调度进程到运行态*/
if(task[next]->pid != current->pid)
{
//判断当前正在运行的进程状态是否为TASK_RUNNING,
//如果是,则表明当前的进程是时间片到期被抢走的,这时当前进程的状态还应是TASK_RUNNING,
//如果不是,则说明当前进程是主动让出CPU,则状态应为其他Wait状态。
if(current->state == TASK_RUNNING)
{
//记录当前进程的状态为J,在此处,当前进程由运行态转变为就绪态。
fprintk(,"%ld\t%c\t%ld\n",current->pid,'J',jiffies);
}
fprintk(,"%ld\t%c\t%ld\n",pnext->pid,'R',jiffies);
}
/**/ //switch_tss(next); //由于此次实验难度还是挺高的,所以一般不会一次成功,所以我没有将switch_to宏删除,
//而只是将其改了一个名字,这样,如果下面的切换出问题,就切换回来测试是否是其他代码出问题了。
//如果换回来正常,则说明问题就出现在下面的切换上。这样可以减少盲目地修改。
switch_to(pnext,_LDT(next));

3、fork.c

主要修改的是copy_process函数,见下面:

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
/*melon - 添加用来取得内核栈指针*/
long * krnstack;
/*melon added End*/
struct task_struct *p;
int i;
struct file *f; p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN; /*melon -取得当前子进程的内核栈指针*/
krnstack=(long)(PAGE_SIZE+(long)p); //实际上进程每次进入内核,栈顶都指向这里。
/*melon added End*/
task[nr] = p;
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
p->father = current->pid;
p->counter = p->priority;
//初始化内核栈内容,由于系统不再使用tss进行切换,所以内核栈内容要自已安排好
//下面部分就是进入内核后int之前入栈内容,即用户态下的cpu现场
*(--krnstack) = ss & 0xffff; //保存用户栈段寄存器,这些参数均来自于此次的函数调用,
//即父进程压栈内容,看下面关于tss的设置此处和那里一样。
*(--krnstack) = esp; //保存用户栈顶指针
*(--krnstack) = eflags; //保存标识寄存器
*(--krnstack) = cs & 0xffff; //保存用户代码段寄存器
*(--krnstack) = eip; //保存eip指针数据,iret时会出栈使用 ,这里也是子进程运行时的语句地址。即if(!fork()==0) 那里的地址,由父进程传递
//下面是iret时要使用的栈内容,由于调度发生前被中断的进程总是在内核的int中,
//所以这里也要模拟中断返回现场,这里为什么不能直接将中断返回时使用的
//return_from_systemcall地址加进来呢?如果完全模仿可不可以呢?
//有空我会测试一下。
//根据老师的视频讲义和实验指导,这里保存了段寄存器数据。
//由switch_to返回后first_return_fromkernel时运行,模拟system_call的返回
*(--krnstack) = ds & 0xffff;
*(--krnstack) = es & 0xffff;
*(--krnstack) = fs & 0xffff;
*(--krnstack) = gs & 0xffff;
*(--krnstack) = esi;
*(--krnstack) = edi;
*(--krnstack) = edx;
//*(--krnstack) = ecx; //这三句是我根据int返回栈内容加上去的,后来发现不加也可以
//但如果完全模拟return_from_systemcall的话,这里应该要加上。
//*(--krnstack) = ebx;
//*(--krnstack) = 0; //此处应是返回的子进程pid//eax;
//其意义等同于p->tss.eax=0;因为tss不再被使用,
//所以返回值在这里被写入栈内,在switch_to返回前被弹出给eax; //switch_to的ret语句将会用以下地址做为弹出进址进行运行
*(--krnstack) = (long)first_return_from_kernel;
//*(--krnstack) = &first_return_from_kernel; //讨论区中有同学说应该这样写,结果同上
//这是在switch_to一起定义的一段用来返回用户态的汇编标号,也就是
//以下是switch_to函数返回时要使用的出栈数据
//也就是说如果子进程得到机会运行,一定也是先
//到switch_to的结束部分去运行,因为PCB是在那里被切换的,栈也是在那里被切换的,
//所以下面的数据一定要事先压到一个要运行的进程中才可以平衡。
*(--krnstack) = ebp;
*(--krnstack) = eflags; //新添加
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = ; //这里的eax=0是switch_to返回时弹出的,而且在后面没有被修改过。
//此处之所以是0,是因为子进程要返回0。而返回数据要放在eax中,
//由于switch_to之后eax并没有被修改,所以这个值一直被保留。
//所以在上面的栈中可以不用再压入eax等数据。
//将内核栈的栈顶保存到内核指针处
p->kernelstack=krnstack; //保存当前栈顶
//p->eip=(long)first_switch_from;
//上面这句是第一次被调度时使用的地址 ,这里是后期经过测试后发现系统修改
//后会发生不定期死机,经分析后认为是ip不正确导致的,但分析是否正确不得
//而知,只是经过这样修改后问题解决,不知其他同学是否遇到这个问题。
/*melon added End*/

4、system_call.s

这个文件中主要是添加新的switch_to函数。见下面:

.align
switch_to:
pushl %ebp
movl %esp,%ebp #上面两条用来调整C函数栈态
pushfl #将当前的内核eflags入栈!!!!
pushl %ecx
pushl %ebx
pushl %eax
movl (%ebp),%ebx #此时ebx中保存的是第一个参数switch_to(pnext,LDT(next))
cmpl %ebx,current #此处判断传进来的PCB是否为当前运行的PCB
je 1f #如果相等,则直接退出
#切换PCB
movl %ebx,%eax #ebx中保存的是传递进来的要切换的pcb
xchgl %eax,current #交换eax和current,交换完毕后eax中保存的是被切出去的PCB
#TSS中内核栈指针重写
movl tss,%ecx #将全局的tss指针保存在ecx中
addl $,%ebx #取得tss保存的内核栈指针保存到ebx中
movl %ebx,ESP0(%ecx) #将内核栈指针保存到全局的tss的内核栈指针处esp0=
#切换内核栈
movl %esp,KERNEL_STACK(%eax) #将切出去的PCB中的内核栈指针存回去
movl $1f,KERNEL_EIP(%eax) #将1处地址保存在切出去的PCB的EIP中!!!!
movl (%ebp),%ebx #重取ebx值,
movl KERNEL_STACK(%ebx),%esp #将切进来的内核栈指针保存到寄存器中
#下面两句是后来添加的,实验指导上并没有这样做。
pushl KERNEL_EIP(%ebx) #将保存在切换的PCB中的EIP放入栈中!!!!
jmp switch_csip #跳到switch_csip处执行!!!!
# 原切换LDT代码换到下面
# 原切换LDT的代码在下面
: popl %eax
popl %ebx
popl %ecx
popfl #将切换时保存的eflags出栈!!!!
popl %ebp
ret #该语句用来出栈ip switch_csip: #用来进行内核段切换和cs:ip跳转
#切换LDT
movl (%ebp),%ecx #取出第二个参数,_LDT(next)
lldt %cx #切换LDT
#切换cs后要重新切换fs,所以要将fs切换到用户态内存
movl $0x17,%ecx #此时ECX中存放的是LDT
mov %cx,%fs
cmpl %eax,last_task_used_math
jne 1f
clts
:
ret #此处用来弹出pushl next->eip!!!!!!!! #第一次被调度时运行的切换代码
first_switch_from:
popl %eax
popl %ebx
popl %ecx
popfl #将切换时保存的eflags出栈!!!!
popl %ebp
ret #该语句用来出栈ip #此处是从内核站中返回时使用的代码,用来做中断返回
first_return_from_kernel:
#popl %eax
#popl %ebx
#popl %ecx
popl %edx
popl %edi
popl %esi
popl %gs
popl %fs
popl %es
popl %ds
iret

此外还要添加几个常量值:

/*melon 添加用来取内核栈指针的位置宏*/
KERNEL_STACK = //内核栈指针在task_struct结构中的位移,指导上这里是12
ESP0 = //tss中内核栈指针位移
KERNEL_EIP = //新添加的task_sturct结构中的eip指针位移
/*melon added End*/

实验指导提出了几个思考问题如下:

问题1、

针对下面的代码片段:

movl tss,%ecx
addl $,%ebx
movl %ebx,ESP0(%ecx)

回答问题:

  • (1)为什么要加4096;
  • (2)为什么没有设置tss中的ss0。

  • 1)该代码片段是用来对全局的tss结构进行进程内核栈指针的保存,虽然不再使用tss进行切换,但由于cpu的设计tr寄存器中必须有一个tss结构的指针指向当前cpu中运行的进程的tss结构,在这个修改版本中仅保留一个全局的tss结构用来存储该指针。而在切换进程时要同时将该指针也切换为相应要运行的进程的内核栈栈项指针,由于进程进入内核态运行时其栈是空的,即由用户态经由int转入内核态时其栈为空,由int等代码对其进行操作,如保存用户栈信息等。所以对于tss结构来说,其内核栈指针应总指向进程内核栈空栈时栈顶,而内核栈栈顶位置就位于分配的内存块的4096位置,即PAGE_SIZE,所以这里要将pcb指针加上该值即是空栈时栈顶指针位置。

  • 2)对于linux0.11来说,内核栈段的选择子永远都是0x10,所以这里不用修改该值。相同意义的代码见fork.c中的p->tss.ss0 = 0x10;,这行代码明确指出对于所有进程ss0都是0x10。

问题2、

针对代码片段:

*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = ;

回答问题:

  • (1)子进程第一次执行时,eax=?为什么要等于这个数?哪里的工作让eax等于这样一个数?
  • 回答:子进程第一次执行时要返回0,即eax=0,这样才能和父进程进行分支运行。所以这里要设置eax=0。在使用tss切换时是由p->tss.eax = 0;代码行来设置这个值的。这里只要在switch_to返回时保证eax中的数据为0即可。其实也可不必要在此设置,也可在模仿iret返回时再进行设置。

  • (2)这段代码中的ebx和ecx来自哪里,是什么含义,为什么要通过这些代码将其写到子进程的内核栈中?

  • 回答:代码段中的ebx、ecx均来自于copry_process函数的调用参数,是在sys_fork之前和之中分段进行入栈的。具体到ebx、ecx两个参数是由system_call函数入栈的。是进入内核态前程序在用户态运行时两个寄存器的状态数据。见下面:
system_call:
cmpl $nr_system_calls-,%eax
ja bad_sys_call
push %ds #保存用户态时的寄存器
push %es
push %fs
pushl %edx
pushl %ecx # 是下面真正的int调用时的参数
pushl %ebx # 调用参数
movl $0x10,%edx # 设置ds、es寄存器到内核空间
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs指向用户空间
mov %dx,%fs
call sys_call_table(,%eax,) #调用系统调用
pushl %eax #返回值
movl current,%eax
cmpl $,state(%eax) # state
jne reschedule
cmpl $,counter(%eax) # counter
je reschedule

就个人理解,这里如果不这么设置似乎也是可以的,但这些寄存器的值必须保存在内核栈中,只要可以保证经由iret再返回到用户态时程序可以正常使用就可以。

  • (3)这段代码中的ebp来自哪里,是什么含义,为什么要做这样的设置?可以不设置吗?为什么?
  • 回答:ebp参数来自于下面程序段中的入栈动作。
sys_fork:
call find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp #入栈,成为copy_process的参数
pushl %eax
call copy_process
addl $,%esp
: ret

观察代码流程,没有发现在这之前对ebp进行过修正,虽然调用过c函数find_empty_process但c函数在返回时会保证ebp值 回到调用之前,所以猜测该值应该是int调用之前进程中的ebp,即进程的用户态的ebp,所以为保证进程在返回用户时能正确处理栈帧,这里要保证该值被正确传递。所以要在这里入栈。在程序切换后时会使用到。

问题3、为什么要在切换完LDT之后要重新设置fs=0x17?而且为什么重设操作要出现在切换完LDT之后,出现在LDT之前又会怎么样?

回答:cpu的段寄存器都存在两类值,一类是显式设置段描述符,另一类是隐式设置的段属性及段限长等值,这些值必须经由movl、lldt、lgdt等操作进行设置,而在设置了ldt后,要将fs显示设置一次才能保证段属性等值正确。

linux0.11改进之四 基于内核栈的进程切换的更多相关文章

  1. 在Linux-0.11中实现基于内核栈切换的进程切换

    原有的基于TSS的任务切换的不足 进程切换的六段论 1 中断进入内核 2 找到当前进程的PCB和新进程的PCB 3 完成PCB的切换 4 根据PCB完成内核栈的切换 5 切换运行资源LDT 6 利用I ...

  2. Linux0.11内核源码——内核态进程切换的改进

    本来想自己写的,但是发现了一篇十分优秀的博客 https://www.cnblogs.com/tradoff/p/5734582.html system_call的源码解析:https://blog. ...

  3. Linux0.11启动过程

    从开机加电,到执行main函数之前的过程 好吧,这里应该是有执行3个汇编的文件,但是我不太了解.囧 从main函数,到启动OK(即可以响应用户操作了) 这个步骤做了3件事情: 创建进程0,使之具备在主 ...

  4. 对Linux0.11 中 进程0 和 进程1分析

    1. 背景 进程的创建过程无疑是最重要的操作系统处理过程之一,很多书和教材上说的最多的还是一些原理的部分,忽略了很多细节.比如,子进程复制父进程所拥有的资源,或者子进程和父进程共享相同的物理页面,拥有 ...

  5. Linux0.11之进程0创建进程1(1)

    进程0是由linus写在操作系统文件中的,是预先写死了的.那么进程0以后的进程是如何创建的呢?本篇文章主要讲述进程0创建进程1的过程. 在创建之前,操作系统先是进行了一系列的初始化,分别为设备号.块号 ...

  6. Linux0.11内核源码——内核态线程(进程)切换的实现

    以fork()函数为例,分析内核态进程切换的实现 首先在用户态的某个进程中执行了fork()函数 fork引发中断,切入内核,内核栈绑定用户栈 首先分析五段论中的第一段: 中断入口:先把相关寄存器压栈 ...

  7. linux0.11内核源码——进程各状态切换的跟踪

    准备工作 1.进程的状态有五种:新建(N),就绪或等待(J),睡眠或阻塞(W),运行(R),退出(E),其实还有个僵尸进程,这里先忽略 2.编写一个样本程序process.c,里面实现了一个函数 /* ...

  8. Linux0.11进程切换和TSS结构

    TSS 全称为task state segment,是指在操作系统进程管理的过程中,进程切换时的任务现场信息.       X86体系从硬件上支持任务间的切换.为此目的,它增设了一个新段:任务状态段( ...

  9. Linux0.11内核剖析--内核体系结构

    一个完整可用的操作系统主要由 4 部分组成:硬件.操作系统内核.操作系统服务和用户应用程序,如下图所示: 用户应用程序是指那些字处理程序. Internet 浏览器程序或用户自行编制的各种应用程序: ...

随机推荐

  1. python中的TypeError错误解决办法

    新手在学习python时候,会遇到很多的坑,下面来具体说说其中一个. 在使用python编写面向对象的程序时,新手可能遇到TypeError: this constructor takes no ar ...

  2. 记录一次bug解决过程:velocity中获取url中的参数

    一.总结 在Webx的Velocity中获取url中参数:$rundata.getRequest().getParameter('userId') 在Webx项目中,防止CSRF攻击(Cross-si ...

  3. 高性能 TCP & UDP 通信框架 HP-Socket v3.4.1

    HP-Socket 是一套通用的高性能 TCP/UDP 通信框架,包含服务端组件.客户端组件和 Agent 组件,广泛适用于各种不同应用场景的 TCP/UDP 通信系统,提供 C/C++.C#.Del ...

  4. session 存入数据库 php

     session 机制 1.php中session的生成机制 session是保存在服务器的,当我们在代码中调用session_start();时,PHP会同时往SESSION的存放目录(默认为/tm ...

  5. 网址http换成https ----js

    <script type="text/javascript"> var url = window.location.href; if (url.indexOf(&quo ...

  6. jquery toggle方法使用出错?请看这里-遁地龙卷风

    这个函数在1.9之前和1.9之后的用法大不相同 1 1.9之间,用于点击元素时函数的轮流执行 toggle(function() { alert(1);//1,3,5,7... },function( ...

  7. iOS---关于UIWebView

    1.加载的web界面可以自由收缩 webView.scalesPageToFit = YES;

  8. 2、项目标准的制定 - PMO项目管理办公室

    PMO项目管理办公室应该是针对公司级别的项目内容制定项目标准.这个由公司的项目经理们进行集中讨论,然后将项目标准进行文档化,标准化,从而为公司的项目进行指导性的功能,为公司的项目进行服务.下面对项目标 ...

  9. Mac下QT错误,Xcode配置解决办法

    出现错误: Xcode not set up properly. You may need to confirm the license agreement by running / 解决办法如下: ...

  10. Ubuntu15.10下华南师大锐捷认证客户端的使用详解

    本文测试Linux系统环境为Ubuntu15.10 Destop,暂未在其他系统下验证过,不代表LTS版本或较旧的版本按照本文所述方法可以100%正确使用最新的锐捷认证客户端. 本文面向对象为华南师范 ...