关于进程和线程的个人理解
概述:
进程是操作系统内资源管理的最小单位,线程是CPU调度的最小单位。
进程所处理的,主要是内存管理和分配问题。
线程所面临的,是进程内上下文切换过程。
内存空间:
进程的内存虚拟地址分配:用户空间(代码段,数据段、堆、动态库地址、栈),内核空间(虚拟地址进程独享,但是实际指向所有进程共享的物理地址)。
进程看到的内存地址是连续的虚拟地址,通过多级页表与实际物理内存地址相映射。
进程 就是一段代码和一系列相关的数据的打包体,他们在启动时被内核(或操作系统)加载并按照固定的排布顺序组织到内存中,在进程自己看来它具有完整的内存空间,但操作系统只是给它分配了专属于它自己的虚拟内存空间,并维持了多级页表来保存虚拟地址到物理地址的映射。
当程序进入启动入口后,实际上是,操作系统将程序的入口函数地址调度到CPU上执行,该地址处的指令因此在CPU上被执行,并访问该程序可以访问的内存空间(数据区、堆、栈等等);
该程序的代码段指令被依次在CPU上执行,期间,可能跳转到动态链接库地址继续执行;可能跳转到系统调用地址(内核空间)上执行系统调用,这时会发生用户态到内核态的上下文切换。
所谓的上下文切换实际上是进程的内核区虚拟地址被CPU调度执行,这时候CPU中用于保存用户空间用到的堆、栈、数据区等虚拟地址的寄存器,要被用来保存内核空间所用到的堆、栈地址,即所谓的CPU上下文切换。
内核地址段的指令和用户空间指令都一样在CPU上被调度执行,但不同之处是,用户空间地址会被操作系统检查,只有自己虚拟地址空间的地址才能被执行,超出时操作系统就会将进程杀死,而内核空间的地址是所有进程共享的,不会有越界检查。正是因为如此,内核空间更受操作系统保护。
查看进程的内存空间映射:
$ cat /proc/<pid>/maps
得到的输出中会有[heap] [stack] 等标识,我们可以清晰的看到该程序的虚拟内存地址空
间,及他们的排布情况。
生命周期:
启动:fork execve
fork出来的进程和父进程共享代码段空间,但是其他的空间会被全部复制。
包括已经被父进程打开的文件描述符也会被复制,同时操作系统维护的文件打开数被+1。
这也就是为什么在父进程中打开的文件需要在父进程中关闭,无关子进程的文件操作。
fork的返回值如果为非0,则代表当前空间是父进程空间,如果为0,说明是子进程空间。
execve是执行另一个程序,加载新程序的地址空间,覆盖当前老程序的地址空间。
这里边也要注意已打开文件的问题,可以通过fnctl设置系统文件描述符标志位:FD_CLOEXEC,执行时关闭。
状态监控:wait
wait:等待子进程退出,有几种变形,像waitpid(可选择是否阻塞),waitid(可监控子进程运行状态)。
即便父进程不关心子进程的退出状态,也要使用wait,原因是,操作系统会保存子进程的终止状态和资源使用,直到父进程使用wait将其取走,如果父进程不取走,子进程会变成僵尸状态,在频繁fork时,这种情况不可接受(会产生大量的僵尸进程)。
这些僵尸进程会随着父进程的退出而退出(因为init进程最终接管这些僵尸进程,并执行wait操作结束他们)。
终止:exit
_exit 和 exit的区别: _exit 仅仅负责退出进程,一般仅在子进程中使用,而exit可以调用注册的个性化回调函数后再退出,一般用在主进程中。
_exit 在退出时会清空所有占用的资源,包括打开的文件描述符。
退出处理程序可以通过 atexit() 或 on_exit() 函数注册。其中 atexit() 只能注册返回值和参数都为空的回调函数,而 on_exit() 可以注册带参数的回调函数。退出处理函数的执行顺序与注册顺序相反。它们的函数原型如下所示:
int atexit(void (func)(void));
int on_exit(void (func)(int, void *), void *arg);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端