内核态与用户态_linux内核态和用户态通信

内核态与用户态_linux内核态和用户态通信1、高位地址:栈(存放着局部变量和函数参数等数据),向下生长   (可读可写可执行)2、           堆(给动态分配内存是使用),向上生长             (可读可写可执行)3、           数据段(保存全局数据和静态数据)                    (可读可写不可执行)4、低位地址:代码段(保存代码)

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

1、高位地址:栈(存放着局部变量和函数参数等数据),向下生长   (可读可写可执行)

2、           堆(给动态分配内存是使用),向上生长             (可读可写可执行)

3、           数据段(保存全局数据和静态数据)                    (可读可写不可执行)

4、低位地址:代码段(保存代码)                                (可读可执行不可写)

 

 

代码段就是存储程序文本的,所以有时候也叫做文本段,指令指针中的指令就是从这里取得。这个段一般是可以被共享的,比如你在Linux开了2个Vi来编辑文本,那么一般来说这两个Vi是共享一个代码段的,但是数据段不同(这点有点类似C++中类的不同对象共享相同成员函数)。

        数据段是存储数据用的,还可以分成初始化为非零的数据区,BSS,和堆(Heap)三个区域。初始化非零数据区域一般存放静态非零数据和全局的非零数据。BSS是Block Started by Symbol的缩写,原本是汇编语言中的术语。该区域主要存放未初始化的全局数据和静态数据。还有就是堆了,这个区域是给动态分配内存是使用的,也就是用malloc等函数分配的内存就是在这个区域里的。它的地址是向上增长的。

        最后一个堆栈段(注意,堆栈是Stack,堆是Heap,不是同一个东西),堆栈可太重要了,这里存放着局部变量和函数参数等数据。例如递归算法就是靠栈实现的。栈的地址是向下增长的。具体如下:

========高地址  =======
程序栈                 //堆栈段

向下增长

“空洞”                =======

向上增长


——                    //数据段
BSS
——
非零数据
=========低地址  =======

=========          =======
代码                    //代码段
=========         =======

        需要注意的是,代码段和数据段之间有明确的分隔,但是数据段和堆栈段之间没有,而且栈是向下增长,堆是向上增长的,因此理论上来说堆和栈会“增长到一起”,但是操作系统会防止这样的错误发生,所以不用过分担心。

 

 

一、预备知识—程序的内存分配 

由C/C++编译的程序占用的内存分为以下几个部分 
1、栈区(stack): 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 
2、堆区(heap): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 
3、全局区(static): 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后有系统释放 。 
4、文字常量区: 常量字符串就是放在这里的, 程序结束后由系统释放。 
5、程序代码区: 存放函数体的二进制代码。 

Example: 

int a = 0; // 全局初始化区 
char *p1; // 全局未初始化区 
main() 

int b; // 栈 
char s[] = “abc”; // 栈 
char *p2; // 栈 
char *p3 = “123456”; // 123456\0在常量区,p3在栈上。 
static int c =0; // 全局(静态)初始化区 
p1 = (char *)malloc(10); 
p2 = (char *)malloc(20); // 分配得来得10和20字节的区域就在堆区。 
strcpy(p1, “123456”); // 123456\0放在常量区,编译器可能会将它与p3所指向的”123456″优化成一个地方。 

二、堆和栈的理论知识 

2.1 申请方式 
栈: 由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间 
堆: 需要程序员自己申请,并指明大小,在c中malloc函数:如p1 = (char *)malloc(10); 在C++中用new运算符 如p2 = (char*)malloc(10); 但是注意p1、p2本身是在栈中的。 

2.2 申请后系统的响应 
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 

2.3 申请大小的限制 
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 

2.4 申请效率的比较: 
栈:由系统自动分配,速度较快。但程序员是无法控制的。 
堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。 
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。 

2.5 堆和栈中的存储内容 
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。 
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 

2.6 存取效率的比较 
char s1[] = “aaaaaaaaaaaaaaa”; 
char *s2 = “bbbbbbbbbbbbbbbbb”; 
aaaaaaaaaaa是在运行时刻赋值的; 
而bbbbbbbbbbb是在编译时就确定的; 
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 
比如: 
#include 
void main() 

char a = 1; 
char c[] = “1234567890”; 
char *p =”1234567890″; 
a = c[1]; 
a = p[1]; 
return; 

对应的汇编代码 
10: a = c[1]; 
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]   //将ptr[ebp-0Fh]传给cl
0040106A 88 4D FC mov byte ptr [ebp-4],cl 
11: a = p[1]; 
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 
00401070 8A 42 01 mov al,byte ptr [edx+1] 
00401073 88 45 FC mov byte ptr [ebp-4],al 
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。 

2.7 小结: 
堆和栈的区别可以用如下的比喻来看出: 使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。 使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。 

还有就是函数调用时会在栈上有一系列的保留现场及传递参数的操作。栈的空间大小有限定,VC的缺省是2M。栈不够用的情况一般是程序中分配了大量数组和递归函数层次太深。有一点必须知道,当一个函数调用完返回后它会释放该函数中所有的栈空间。栈是由编译器自动管理的,不用你操心。堆是动态分配内存的,并且你可以分配使用很大的内存。但是用不好会产生内存泄漏。并且频繁地malloc和free会产生内存碎片(有点类似磁盘碎片),因为C分配动态内存时是寻找匹配的内存的。而用栈则不会产生碎片。在栈上存取数据比通过指针在堆上存取数据快些。一般大家说的堆栈和栈是一样的,就是栈(stack),而说堆时才是堆heap。栈是先入后出的,一般是由高地址向低地址生长。 

 

 

 

(1)让我们忽略Linux对段式内存映射的支持。在保护模式下,我们知道无论CPU运行于用户态还是核心态,CPU执行程序所访问的地址都是虚拟地址,MMU 必须通过读取控制寄存器CR3中的值作为当前页面目录的指针,进而根据分页内存映射机制(参看相关文档)将该虚拟地址转换为真正的物理地址才能让CPU真正的访问到物理地址。

(2)对于32位的Linux,其每一个进程都有4G的寻址空间,但当一个进程访问其虚拟内存空间中的某个地址时又是怎样实现不与其它进程的虚拟空间混淆的呢?每个进程都有其自身的页面目录PGD,Linux将该目录的指针存放在与进程对应的内存结构task_struct.(struct mm_struct)mm->pgd中。每当一个进程被调度(schedule())即将进入运行态时,Linux内核都要用该进程的PGD指针设置CR3(switch_mm())。

(3)当创建一个新的进程时,都要为新进程创建一个新的页面目录PGD,并从内核的页面目录swapper_pg_dir中复制内核区间页面目录项至新建进程页面目录PGD的相应位置,具体过程如下:
do_fork() –> copy_mm() –> mm_init() –> pgd_alloc() –>set_pgd_fast() –> get_pgd_slow() –> memcpy(&PGD +USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER_PGD -USER_PTRS_PER_PGD) * sizeof(pgd_t))
这样一来,每个进程的页面目录就分成了两部分,第一部分为“用户空间”,用来映射其整个进程空间(0x0000 0000-0xBFFF FFFF)即3G字节的虚拟地址;第二部分为“系统空间”,用来映射(0xC000 0000-0xFFFF FFFF)1G字节的虚拟地址。可以看出Linux系统中每个进程的页面目录的第二部分是相同的,所以从进程的角度来看,每个进程有4G字节的虚拟空间,较低的3G字节是自己的用户空间,最高的1G字节则为与所有进程以及内核共享的系统空间。

(4)现在假设我们有如下一个情景:
在进程A中通过系统调用sethostname(constchar *name,seze_t len)设置计算机在网络中的“主机名”.
在该情景中我们势必涉及到从用户空间向内核空间传递数据的问题,name是用户空间中的地址,它要通过系统调用设置到内核中的某个地址中。让我们看看这个过程中的一些细节问题:系统调用的具体实现是将系统调用的参数依次存入寄存器ebx,ecx,edx,esi,edi(最多5个参数,该情景有两个 name和len),接着将系统调用号存入寄存器eax,然后通过中断指令“int 80”使进程A进入系统空间。由于进程的CPU运行级别小于等于为系统调用设置的陷阱门的准入级别3,所以可以畅通无阻的进入系统空间去执行为int80设置的函数指针system_call()。由于system_call()属于内核空间,其运行级别DPL为0,CPU要将堆栈切换到内核堆栈,即进程A的系统空间堆栈。我们知道内核为新建进程创建task_struct结构时,共分配了两个连续的页面,即8K的大小,并将底部约1k的大小用于 task_struct(如#definealloc_task_struct() ((struct task_struct *) __get_free_pages(GFP_KERNEL,1))),而其余部分内存用于系统空间的堆栈空间,即当从用户空间转入系统空间时,堆栈指针 esp变成了(alloc_task_struct()+8192),这也是为什么系统空间通常用宏定义current(参看其实现)获取当前进程的 task_struct地址的原因。每次在进程从用户空间进入系统空间之初,系统堆栈就已经被依次压入用户堆栈SS、用户堆栈指针ESP、EFLAGS、用户空间CS、EIP,接着system_call()将eax压入,再接着调用SAVE_ALL依次压入ES、DS、EAX、EBP、EDI、ESI、 EDX、ECX、EBX,然后调用sys_call_table+4*%EAX,本情景为sys_sethostname()。

(5)在sys_sethostname()中,经过一些保护考虑后,调用copy_from_user(to,from,n),其中to指向内核空间system_utsname.nodename,譬如0xE625A000,from指向用户空间譬如0x8010FE00。现在进程A进入了内核,在系统空间中运行,MMU根据其PGD将虚拟地址完成到物理地址的映射,最终完成从用户空间到系统空间数据的复制。准备复制之前内核先要确定用户空间地址和长度的合法性,至于从该用户空间地址开始的某个长度的整个区间是否已经映射并不去检查,如果区间内某个地址未映射或读写权限等问题出现时,则视为坏地址,就产生一个页面异常,让页面异常服务程序处理。过程如下:copy_from_user()->generic_copy_from_user()->access_ok()+__copy_user_zeroing().

(6)小结:
*进程寻址空间0~4G  
*进程在用户态只能访问0~3G,只有进入内核态才能访问3G~4G  
*进程通过系统调用进入内核态
*每个进程虚拟空间的3G~4G部分是相同的  
*进程从用户态进入内核态不会引起CR3的改变但会引起堆栈的改变

 

5.8 Linux 系统中堆栈的使用方法

本节内容概要描述了Linux内核从开机引导到系统正常运行过程中对堆栈的使用方式。这部分内容的说明与内核代码关系比较密切,可以先跳过。在开始阅读相应代码时再回来仔细研究。

Linux 0.12系统中共使用了4种堆栈。第1种是系统引导初始化时临时使用的堆栈;第2种是进入保护模式之后提供内核程序初始化使用的堆栈,位于内核代码地址空间固定位置处。该堆栈也是后来任务0使用的用户态堆栈;第3种是每个任务通过系统调用,执行内核程序时使用的堆栈,我们称之为任务的内核态堆栈。每个任务都有自己独立的内核态堆栈;第4种是任务在用户态执行的堆栈,位于任务(进程)逻辑地址空间近末端处。

使用多个栈或在不同情况下使用不同栈的主要原因有两个。首先是由于从实模式进入保护模式,使得CPU对内存寻址访问方式发生了变化,因此需要重新调整设置栈区域。另外,为了解决不同CPU特权级共享使用堆栈带来的保护问题,执行0级的内核代码和执行3级的用户代码需要使用不同的栈。当一个任务进入内核态运行时,就会使用其TSS段中给出的特权级0的堆栈指针tss.ss0、tss.esp0,即内核栈。原用户栈指针会被保存在内核栈中。而当从内核态返回用户态时,就会恢复使用用户态的堆栈。下面分别对它们进行说明。

5.8.1  初始化阶段

(1)开机初始化时(bootsect.S,setup.s)

当bootsect代码被ROM BIOS引导加载到物理内存0x7c00处时,并没有设置堆栈段,当然程序也没有使用堆栈。直到bootsect被移动到0x9000:0处时,才把堆栈段寄存器SS设置为0x9000,堆栈指针esp寄存器设置为0xff00,即堆栈顶端在0x9000:0xff00处,参见boot/bootsect.s第61、62行。setup.s程序中也沿用了bootsect中设置的堆栈段。这就是系统初始化时临时使用的堆栈。

(2)进入保护模式时(head.s)

从head.s程序起,系统开始正式在保护模式下运行。此时堆栈段被设置为内核数据段(0x10),堆栈指针esp设置成指向user_stack数组的顶端(参见head.s,第31行),保留了1页内存(4KB)作为堆栈使用。user_stack数组定义在sched.c的67~72行,共含有1024个长字。它在物理内存中的位置示意图可参见图5-23。此时该堆栈是内核程序自己使用的堆栈。其中给出的地址是大约值,它们与编译时的实际设置参数有关。这些地址位置是从编译内核时生成的system.map文件中查到的。

5-23  刚进入保护模式时内核使用的堆栈示意图

(3)初始化时(main.c)

在init/main.c程序中,在执行move_to_user_mode()代码把控制权移交给任务0之前,系统一直使用上述堆栈。而在执行过move_to_user_mode()之后,main.c的代码被“切换”成任务0中执行。通过执行fork()系统调用,main.c中的init()将在任务1中执行,并使用任务1的堆栈。而main()本身则在被“切换”成为任务0后,仍然继续使用上述内核程序自己的堆栈作为任务0的用户态堆栈。关于任务0所使用堆栈的详细描述见后面说明。

5.8.2 任务的堆栈

每个任务都有两个堆栈,分别用于用户态和内核态程序的执行,并且分别称为用户态堆栈和内核态堆栈。除了处于不同CPU特权级中,这两个堆栈之间的主要区别在于任务的内核态堆栈很小,所保存的数据量最多不能超过4096 – 任务数据结构块个字节,大约为3KB。而任务的用户态堆栈却可以在用户的64MB空间内延伸。

(1)在用户态运行时

每个任务(除了任务0和任务1)有自己的64MB地址空间。当一个任务(进程)刚被创建时,它的用户态堆栈指针被设置在其地址空间的靠近末端(64MB顶端)部分。实际上末端部分还要包括执行程序的参数和环境变量,然后才是用户堆栈空间,如图5-24所示。应用程序在用户态下运行时就一直使用这个堆栈。堆栈实际使用的物理内存则由CPU分页机制确定。由于Linux实现了写时复制功能(Copy on Write),因此在进程被创建后,若该进程及其父进程都没有使用堆栈,则两者共享同一堆栈对应的物理内存页面。只有当其中一个进程执行堆栈写操作(如push操作)时内核内存管理程序才会为写操作进程分配新的内存页面。而进程0和进程1的用户堆栈比较特殊,见后面说明。

5-24  逻辑空间中的用户态堆栈

(2)在内核态运行时

每个任务都有自己的内核态堆栈,用于任务在内核代码中执行期间。其所在线性地址中的位置由该任务TSS段中ss0和esp0两个字段指定。ss0是任务内核态堆栈的段选择符,esp0是堆栈栈底指针。因此每当任务从用户代码转移进入内核代码中执行时,任务的内核态栈总是空的。任务内核态堆栈被设置在位于其任务数据结构所在页面的末端,即与任务的任务数据结构(task_struct)放在同一页面内。这是在建立新任务时,fork()程序在任务tss段的内核级堆栈字段(tss.esp0和tss.ss0)中设置的,参见kernel/fork.c,92行:

    p->tss.esp0= PAGE_SIZE + (long)p;

   p->tss.ss0 = 0x10;

其中,p是新任务的任务数据结构指针,tss是任务状态段结构。内核为新任务申请内存用作保存其task_struct结构数据,而tss结构(段)是task_struct中的一个字段。该任务的内核堆栈段值tss.ss0也被设置成为0x10(即内核数据段选择符),而tss.esp0则指向保存task_struct结构页面的末端。如图5-25所示。实际上tss.esp0被设置成指向该页面(外)上一字节处(图中堆栈底处)。这是因为Intel CPU执行堆栈操作时是先递减堆栈指针esp值,然后在esp指针处保存入栈内容。

5-25  进程的内核态堆栈示意图

为什么从主内存区申请得来的用于保存任务数据结构的一页内存也能被设置成内核数据段中的数据呢,即tss.ss0为什么能被设置成0x10呢?这是因为用户内核态栈仍然属于内核数据空间。我们可以从内核代码段的长度范围来说明。在head.s程序的末端,分别设置了内核代码段和数据段的描述符,段长度都被设置成了16MB。这个长度值是Linux 0.12内核所能支持的最大物理内存长度(参见head.s,110行开始的注释)。因此,内核代码可以寻址到整个物理内存范围中的任何位置,当然也包括主内存区。每当任务执行内核程序而需要使用其内核栈时,CPU就会利用TSS结构把它的内核态堆栈设置成由tss.ss0和tss.esp0这两个值构成。在任务切换时,老任务的内核栈指针esp0不会被保存。对CPU来讲,这两个值是只读的。因此每当一个任务进入内核态执行时,其内核态堆栈总是空的。

(3)任务0和任务1的堆栈

任务0(空闲进程idle)和任务1(初始化进程init)的堆栈比较特殊,需要特别予以说明。任务0和任务1的代码段和数据段相同,限长也都是640KB,但它们被映射到不同的线性地址范围中。任务0的段基地址从线性地址0开始,而任务1的段基地址从64MB开始。但是它们全都映射到物理地址0~640KB范围中。这个地址范围也就是内核代码和基本数据所存放的地方。在执行了move_to_user_mode()之后,任务0和任务1的内核态堆栈分别位于各自任务数据结构所在页面的末端,而任务0的用户态堆栈就是前面进入保护模式后所使用的堆栈,即sched.c的user_stack[]数组的位置。由于任务1在创建时复制了任务0的用户堆栈,因此刚开始时任务0和任务1共享使用同一个用户堆栈空间。但是当任务1开始运行时,由于任务1映射到user_stack[]处的页表项被设置成只读,使得任务1在执行堆栈操作时将会引起写页面异常,从而内核会使用写时复制机制(关于写时复制技术的说明请参见第13章)为任务1另行分配主内存区页面作为堆栈空间使用。只有到此时,任务1才开始使用自己独立的用户堆栈内存页面。因此任务0的堆栈需要在任务1实际开始使用之前保持“干净”,即任务0此时不能使用堆栈,以确保复制的堆栈页面中不含有任务0的数据。

任务0的内核态堆栈是在其人工设置的初始化任务数据结构中指定的,而它的用户态堆栈是在执行move_to_user_mode()时,在模拟iret返回之前的堆栈中设置的,参见图5-22所示。我们知道,当进行特权级会发生变化的控制权转移时,目的代码会使用新特权级的堆栈,而原特权级代码堆栈指针将保留在新堆栈中。因此这里先把任务0用户堆栈指针压入当前处于特权级0的堆栈中,同时把代码指针也压入堆栈,然后执行IRET指令即可实现把控制权从特权级0的代码转移到特权级3的任务0代码中。在这个人工设置内容的堆栈中,原esp值被设置成仍然是user_stack中原来的位置值,而原ss段选择符被设置成0x17,即设置成用户态局部表LDT中的数据段选择符。然后把任务0代码段选择符0x0f压入堆栈作为栈中原CS段的选择符,把下一条指令的指针作为原EIP压入堆栈。这样,通过执行IRET指令即可“返回”到任务0的代码中继续执行了。

5.8.3 任务内核态堆栈与用户态堆栈之间的切换

在Linux 0.12系统中,所有中断服务程序都属于内核代码。如果一个中断产生时任务正在用户代码中执行,那么该中断就会引起CPU特权级从3级到0级的变化,此时CPU就会进行用户态堆栈到内核态堆栈的切换操作。CPU会从当前任务的任务状态段TSS中取得新堆栈的段选择符和偏移值。因为中断服务程序在内核中,属于0级特权级代码,所以48位的内核态堆栈指针会从TSS的ss0和esp0字段中获得。在定位了新堆栈(内核态堆栈)之后,CPU就会首先把原用户态堆栈指针ss和esp压入内核态堆栈,随后把标志寄存器eflags的内容和返回位置cs、eip压入内核态堆栈。

内核的系统调用是一个软件中断,因此任务调用系统调用时就会进入内核并执行内核中的中断服务代码。此时内核代码就会使用该任务的内核态堆栈进行操作。同样,当进入内核程序时,由于特权级别发生了改变(从用户态转到内核态),用户态堆栈的堆栈段和堆栈指针以及eflags会被保存在任务的内核态堆栈中。而在执行iret退出内核程序返回到用户程序时,将恢复用户态的堆栈和eflags。这个过程如图5-26所示。

5-26  内核态和用户态堆栈的切换

如果一个任务正在内核态中运行,那么若CPU响应中断就不再需要进行堆栈切换操作,因为此时该任务运行的内核代码已经在使用内核态堆栈,并且不涉及优先级别的变化,所以CPU仅把eflags和中断返回指针cs、eip压入当前内核态堆栈,然后执行中断服务过程。

 

 

 

 

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/192336.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 每天一道算法_3_487-3279_对电话号码格式化统计批处理

    早上弄了一道求高精度幂的算法,偷懒用了内部类,总觉得过意不去,所以今天重新做了一道算法题,做完心里舒服好多。题目如下: Description企业喜欢用容易被记住的电话号码。让电话号码容易被记住的一个办法是将它写成一个容易记住的单词或者短语。例如,你需要给滑铁卢大学打电话时,可以拨打TUT-GLOP。有时,只将电话号码中部分数字拼写成单词。当你晚上回到酒店,可以通过拨打310-GI

  • 显示适配器virtual display device_电脑怎么设置虚拟显示器

    显示适配器virtual display device_电脑怎么设置虚拟显示器根据包括微软研究院在内的许多最新研究,多显示器系统以及更大的显示器可将用户工作效率提高10%至50%。然而,多显示器采用的增长以及大屏幕显示器受到以下几个因素的影响:虚拟显示管理器通过额外的虚拟显示器的便利性来补充您现有的单显示器或多显示器系统,这些虚拟显示器可以细分现有的物理屏幕,同时使用现有硬件以及单击按钮调用不同显示器配置的能力。VDM的虚拟显示技术提供了物理显示器的外观和感觉,并且需要对最终用户进行最少的重新培训。许可证区别虚拟显示管理器提供两个不同的许可证。CL–控制台许可证和MUL

  • windows批处理脚本_批处理打开文件命令

    windows批处理脚本_批处理打开文件命令 Windows平台批处理命令教程 do2jiang@gmail.com蒋冬冬 收集于网络 2009.4  该教程一共分为4大部分,第一部分是批处理的专用命令,第二部分是特殊的符号与批处理,第三部分是批处理与变量,第四部分是完整案例。 第一部分:批处理的专用命令 批处理文件是将一系列命令按一定的顺序集合为一个可执行的文本文件,其扩

  • VC++消息钩子编程「建议收藏」

    VC++消息钩子编程「建议收藏」一、消息钩子的概念1、基本概念Windows应用程序是基于消息驱动的,任何线程只要注册窗口类都会有一个消息队列用于接收用户输入的消息和系统消息。为了拦截消息,Windows提出了钩子的概念。钩子(H

  • pycharm无法使用第三方库_python第三方库安装失败

    pycharm无法使用第三方库_python第三方库安装失败前言:在使用pycharm学习python的时候,经常需要第三方库,没有第三方库程序就会报错,pycharm也会提醒你要安装所需要的库,安装第三方库的时候往往就出现了问题,如图;下面我将谈谈一些我遇到的问题及解决经验。1.检查pip是否为最新版本pip不是最新版本时,会直接导致你安装第三方库失败。解决方法:第一,看是否直接在pycharm的setting里面Project下的Projec…

  • 免裁券也不灵?老板:想不到我裁你两次吧……

    免裁券也不灵?老板:想不到我裁你两次吧……

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号