进程管理(电脑进程管理器)
过程管理(计算机过程管理器)
linux服务器开发的视频分析;
30度讲解流程管理无死角,调度器五种实现
多线程linux的epoll原理分析及reactor的原理与应用
#include#include#include#includevoidmain(){charstrStr =子进程中的bello=”hello”;pid_tpid=fork();if(pid==0){str[0]=’b’;printf(“子进程中str=%s\n”,str);printf(“子进程中str指向的首地址:%x\n”,(unsignedint)str);}else{sleep(1);printf(“父进程中str=%s\n”,str);printf(“父进程中str指向的首地址:%x\n”,(unsignedint)str);}}
[6]
子进程中str指向的头地址:bfdbfc06
Str =父进程中的hello
父进程中str指向的头地址:bfdbfc06
1.背景介绍
这涉及到物理地址和逻辑地址(或虚拟地址)的概念。
从逻辑地址到物理地址的映射称为地址重定向。分为:
静态重定向——当程序装入主存时,逻辑地址到物理地址的转换已经完成,在程序执行过程中不会再发生变化。
动态重定向——在程序执行过程中完成,其实现依赖于硬件地址翻译机制,如基址寄存器。
逻辑地址:CPU产生的地址。CPU产生的逻辑地址分为:p(页号),包含物理内存中每页的基址,作为页表的索引;d(页面偏移量)与基址相结合,用于确定发送到内存设备的物理内存地址。
物理地址:内存单元看到的地址。用户程序看不到真实的物理地址。用户只生成逻辑地址,认为进程空的地址是0到max。物理地址范围从R+0到R+max,R为基址。地址映射——将程序address 空中使用的逻辑地址转换为内存中的物理地址的过程。这是由内存管理单元(MMU)完成的。
可执行程序在存储时分为代码区、数据区和未初始化数据区(不调入内存)。
(1)代码区存储CPU执行的机器指令。通常情况下,代码区是共享的,即其他被执行人可以调用。代码段/文本段通常是只读的,有些架构可以自己修改。
(2)数据区存储初始化的全局变量、静态变量(包括全局和局部变量)和常量。静态全局变量和静态函数只能在当前文件中调用。
(3)未初始化数据区(BSS)存储全局未初始化变量。在程序开始执行之前,BSS的数据被初始化为0或NULL。
代码区空的地址最低,其次是数据区和BSS区,数据区和BSS区在内存中是挨着的。
文本和数据段在编译时已经被分配空,而bss段不占用可执行文件的大小,由链接器访问。
BSS(data not manually initialized)不为该段数据分配空,只记录数据所需的空大小。
数据(手动初始化的数据)段在数据分配空之间,数据存储在目标文件中。
部分包含初始化的全局变量及其值。BSS段的大小是从可执行文件中获取的,然后链接器获取这个大小的内存块,这个内存块就在数据段的紧后面。当这个存储区进入程序的地址空时,它就全部被清除。包括数据段和BSS段在内的整个段此时通常称为数据区。
运行时可执行程序中还有两个区域:堆栈区和堆区。
(4)堆栈区域。编译器自动释放和存储函数的参数值和局部变量。每当调用一个函数时,函数的返回类型和调用的一些信息都存储在堆栈中。然后被调用的函数在栈上为它的自动变量和临时变量分配空。每个被调用的函数都将使用一个新的堆栈。堆栈区域从高地址位向低地址位增长,是一个连续的内部区域。最大容量由系统预先定义。当应用的栈空超过这个限制时,会提示溢出,用户可以从栈中获取更小的空。
(5)堆区。用于动态内存分配,地址位位于BSS和堆栈中间。程序员申请分配(malloc)和释放(free)。堆从低地址位增长到高地址位,采用链式存储结构。频繁的malloc/free导致内存空不连续,产生碎片。在申请heap 空时,库函数按照一定的算法搜索可用的足够大的空。所以堆的效率比栈低很多。
【文章福利】需要C/C++ Linux服务器架构师学习资料添加群812855908(资料包括C/C++、Linux、golang技术、Nginx、ZeroMQ、MySQL、Redis、fastdfs、MongoDB、ZK、流媒体、CDN、P2P、K8S、Docker、TCP/IP、协议
2、叉子
Fork()会产生一个和父进程完全一样的子进程,但是子进程会在后面多次调用exec系统。为了提高效率,linux引入了“写时复制”技术,即只有当进程空之间每个段的内容发生变化时,才会将父进程的内容复制到子进程中。在fork之后和exec之前,这两个进程使用同一个物理空空间(内存区域)。子进程的代码段、数据段、堆栈都指向父进程的物理空室。也就是说,两个进程的虚空是不同的,但是它们对应的物理空。当父子进程改变对应的段时,会将物理空空间分配给子进程对应的段。如果不是为了exec,内核会把对应的物理空室分配给子进程的数据段和堆栈段(到目前为止,两者各有自己的process 空室,互不影响),代码段继续共享父进程的。如果是因为exec,子进程的代码段也会因为两者执行的代码不同而被分配到单独的物理空房间。
fork时,子进程得到父进程data 空、heap和stack的副本,所以变量的地址(当然是虚拟地址)是一样的。
每个进程都有自己的虚拟地址空,不同进程的同一个虚拟地址显然可以对应不同的物理地址。所以地址相同(虚拟地址)但值不同也就不足为奇了。
具体流程如下:
fork子进程完全复制父进程的stack 空,也复制页表,但不复制物理页。所以这个时候虚拟地址和物理地址是一样的,只是父子共享的页面会被标记为“只读”(类似于mmap的私有方式)。如果父进程和子进程总是同一个页面,并且知道它们中的任何一个都将“写入”共享页面,那么将原始的只读页面标记为“可写”,并将其留给另一个进程。
这称为“写入时复制”。因为fork采用了这种写时复制的机制,那么fork从子进程出来后,先调度哪个父子进程呢?一般内核会先调度子进程,因为很多情况下子进程会立即执行exec,这样会清空空栈和堆。。在这些父进程共享的空之间加载新的代码段。。。,这避免了“写入时复制”复制共享页的机会。如果父进程调度可能首先被写入的共享页面,它将产生“写入时复制”的无用性。因此,通常情况下,子进程调度首先下降。
假设父进程malloc的指针指向0x12345678,fork后,子进程中的指针也指向0x12345678,但这两个地址是虚拟内存地址,内存地址转换后对应的物理地址不同。所以这两个城市的两个地址互不相干。
(注1:理解时可以认为fork后这两个相同的虚拟地址指向不同的物理地址,便于理解父子进程之间的独立性)
(注2:但实际上为了提高fork的效率,linux采用了写时复制技术。fork之后,这两个虚拟地址实际上指向同一个物理地址(内存页面)。只有在任何进程试图修改这个虚拟地址的内容之前,这两个虚拟地址才会指向不同的物理地址(新物理地址的内容是从原始物理地址复制的)
3.高管家族
exec系列有六个功能,分别是:
(1)intexecl(constchar*path,constchar*arg,……);(2)intexecle(constchar*path,constchar*arg,……,char*constenvp[]);(3)intexecv(constchar*path,char*constargv[]);(4)intexecve(constchar*filename,char*constargv[],char*constenvp[]);(5)intexecvp(constchar*file,char*constargv[]);(6)intexeclp(constchar*file,constchar*arg,……);
只有execve是真正的系统调用,其他都是基于它的打包库函数。
函数族的作用是根据指定的文件名找到可执行文件,并用它替换调用进程的内容,换句话说就是在调用进程内部执行一个可执行文件。这里的可执行文件可以是一个二进制文件,也可以是在Linux下可执行的任何脚本文件。
与一般情况不同的是,exec函数族的函数在成功执行后是不会返回的,因为调用进程的实体,包括代码段、数据段、栈,都被新的内容替换了,只留下了进程ID等一些表面信息,颇像“三十六计”中的“金蝉脱壳”。它看起来像一个旧的身体,但它被注入了新的灵魂。只有当调用失败时,它们才会返回-1,从原程序的调用点开始执行。