应用编程之进程(一)

一、main函数由谁调用?

操作系统下的应用程序在运行 main() 函数之前需要先执行一段引导代码,最终由这段引导代码去调用应用 程序的 main() 函数,我们在编写应用程序的时候,不用考虑引导代码的问题,在编译链接时,由链接器将引导代码链接到我们的应用程序当中,一起构成最终的可执行文件。
再来看看 argc 和 argv 传参是如何实现的呢?譬如 ./app arg1 arg2 ,这两个参数 arg1 和 arg2 是如何传递给应用程序的 main 函数的呢?当在终端执行程序时,命令行参数( command-line argument ) shell 进程逐一进行解析,shell 进程会将这些参数传递给加载器,加载器加载应用程序时会将其传递给应用程序引导代码,当引导程序调用 main()函数时,在由它最终传递给 main() 函数,如此一来,在我们的应用程序当中便可以获取到命令行参数了。

二、进程号

Linux 系统下的每一个进程都有一个进程号( processID ,简称 PID ),进程号是一个正数,用于唯一标识系统中的某一个进程。在 Ubuntu 系统下执行 ps 命令可以查到系统中进程相关的一些信息,包括每个进程的进程号。

三、进程的虚拟地址

在 Linux 系统中,采用了虚拟内存管理技术,事实上大多数现在操作系统都是如此!在 Linux 系统中,每一个进程都在自己独立的地址空间中运行,在 32 位系统中,每个进程的逻辑地址空间均为 4GB ,这 4GB 的内存空间按照 3:1 的比例进行分配,其中用户进程享有 3G 的空间,而内核独自享有剩下的 1G 空间。
虚拟地址会通过硬件 MMU (内存管理单元)映射到实际的物理地址空间中,建立虚拟地址到物理地址的映射关系后,对虚拟地址的读写操作实际上就是对物理地址的读写操作。
Linux 系统下,应用程序运行在一个虚拟地址空间中,所以程序中读写的内存地址对应也是虚拟地址,并不是真正的物理地址,譬如应用程序中读写 0x80800000 这个地址,实际上并不对应于硬件的 0x80800000这个物理地址。
为什么需要引入虚拟地址呢?
内存使用效率低 。内存空间不足时,就需要将其它程序暂时拷贝到硬盘中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率就会非常低。
进程地址空间不隔离 。由于程序是直接访问物理内存的,所以每一个进程都可以修改其它进程的内存数据,甚至修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成一些破坏,系统不安全、不稳定。
无法确定程序的链接地址 。程序运行时,链接地址和运行地址必须一致,否则程序无法运行!因为程序代码加载到内存的地址是由系统随机分配的,是无法预知的,所以程序的运行地址在编译程序时是无法确认的。
针对以上的一些问题,就引入了虚拟地址机制,程序访问存储器所使用的逻辑地址就是虚拟地址,通过 逻辑地址映射到真正的物理内存上。所有应用程序运行在自己的虚拟地址空间中,使得进程的虚拟地址空 间和物理地址空间隔离开来,这样做带来了很多的 优点
⚫ 进程与进程、进程与内核相互隔离。一个进程不能读取或修改另一个进程或内核的内存数据,这是 因为每一个进程的虚拟地址空间映射到了不同的物理地址空间。提高了系统的安全性与稳定性。
⚫ 在某些应用场合下,两个或者更多进程能够共享内存。因为每个进程都有自己的映射表,可以让不 同进程的虚拟地址空间映射到相同的物理地址空间中。通常,共享内存可用于实现进程间通信。
⚫ 便于实现内存保护机制。譬如在多个进程共享内存时,允许每个进程对内存采取不同的保护措施, 例如,一个进程可能以只读方式访问内存,而另一进程则能够以可读可写的方式访问。
⚫ 编译应用程序时,无需关心链接地址。前面提到了,当程序运行时,要求链接地址与运行地址一致,在引入了虚拟地址机制后,便无需关心这个问题。

四、环境变量的作用

环境变量常见的用途之一是在 shell 中,每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表示用户的家目录,USER 环境变量表示当前用户名, SHELL 环境变量表示 shell 解析器名称, PWD 环境变量表示当前所在目录等,在我们自己的应用程序当中,也可以使用进程的环境变量。

五、fork()创建子进程

一个现有的进程可以调用 fork() 函数创建一个新的进程,调用 fork() 函数的进程称为父进程,由 fork() 函数创建出来的进程被称为子进程(child process ), fork() 函数原型如下所示( fork() 为系统调用):
#include <unistd.h>
pid_t fork(void);
理解 fork() 系统调用的关键在于,完成对其调用后将存在两个进程,一个是原进程(父进程)、另一个 则是创建出来的子进程,并且每个进程都会从 fork() 函数的返回处继续执行,会导致调用 fork() 返回两次值, 子进程返回一个值、父进程返回一个值。在程序代码中,可通过返回值来区分是子进程还是父进程。 fork()调用成功后,将会在父进程中返回子进程的 PID ,而在子进程中返回值是 0 ;如果调用失败,父进 程返回值-1 ,不创建子进程,并设置 errno 。
fork() 调用成功后,子进程和父进程会继续执行 fork() 调用之后的指令,子进程、父进程各自在自己的进程空间中运行。事实上,子进程是父进程的一个副本 ,譬如子进程拷贝了父进程的数据段、堆、栈以及继承 了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间,这是子进程对父进程相应部分存储空间的完全复制,执行 fork() 之后,每个进程均可修改各自的栈数据以及堆段中的变量,而并不影响另一个进程。
虽然子进程是父进程的一个副本,但是对于程序代码段(文本段)来说,两个进程执行相同的代码段, 因为代码段是只读的,也就是说父子进程共享代码段,在内存中只存在一份代码段数据。
fork() 函数调用完成之后,父进程、子进程会各自继续执行 fork() 之后的指令,最终父进程会执行到 exit() 结束进程,而子进程则会通过**_exit()** 结束进程。
fork() 函数使用场景
fork() 函数有以下两种用法:
⚫ 父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常 见的,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用 fork() 创建一个子 进程,使子进程去处理此请求、而父进程可以继续等待下一个服务请求。
⚫ 一个进程要执行不同的程序。譬如在程序 app1 中调用 fork() 函数创建了子进程,此时子进程是要 去执行另一个程序 app2 ,也就是子进程需要执行的代码是 app2 程序对应的代码,子进程将从 app2 程序的 main 函数开始运行。这种情况,通常在子进程从 fork() 函数返回之后立即调用 exec 族函数来实现

六、系统调用****vfork()

除了 fork() 系统调用之外, Linux 系统还提供了 vfork() 系统调用用于创建子进程, vfork() 与 fork() 函数在 功能上是相同的,并且返回值也相同,在一些细节上存在区别。
之前可以将 fork() 认作对父进程的数据段、堆段、栈段以及其它一些数据结构创建拷贝,由此可以看出,使用 fork() 系统调用的代价是很大的,它复制了父进程中的数据段和堆栈段中的绝大部分内容,这将会消耗比较多的时间,效率会有所降低,而且太浪费,原因有很多,其中之一在于,fork() 函数之后子进程通常会调用 exec 函数,也就是 fork() 第二种使用场景下,这使得子进程不再执行父程序中的代码段,而是执行新程序的代码段,从新程序的 main 函数开始执行、并为新程序重新初始化其数据段、堆段、 栈段等;那么在这种情况下,子进程并不需要用到父进程的数据段、堆段、栈段(譬如父程序中定义的局部变量、全局变量等)中的数据,此时就会导致浪费时间、效率降低。
引入了 vfork() 系统调用,虽然在一些细节上有所不同,但其效率要高于 fork() 函数。类似于 fork() , vfork() 可以为调用该函数的进程创建一个新的子进程,然而, vfork() 是为子进程立即执行 exec() 新的程序而专门设计的,也就是 fork() 函数的第二个使用场景。
vfork()与 fork()函数主要有以下两个区别:
⚫ vfork() 与 fork() 一样都创建了子进程,但 vfork() 函数并不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec (或 _exit ),于是也就不会引用该地址空间的数据。不过在子进程调用 exec 或 _exit 之前,它在父进程的空间中运行、子进程共享父进程的内存。这种优化工作方式 的实现提高的效率;但如果子进程修改了父进程的数据(除了 vfork 返回值的变量)、进行了函数
调用、或者没有调用 exec 或 _exit 就返回将可能带来未知的结果。
⚫ 另一个区别在于, vfork() 保证子进程先运行,子进程调用 exec 之后父进程才可能被调度运行。 虽然 vfork() 系统调用在效率上要优于 fork() ,但是 vfork() 可能会导致一些难以察觉的程序 bug ,所以尽 量避免使用 vfork() 来创建子进程,虽然 fork() 在效率上并没有 vfork() 高,但是现代的 Linux 系统内核已经采 用了写时复制技术来实现 fork() ,其效率较之于早期的 fork() 实现要高出许多,除非速度绝对重要的场合, 我们的程序当中应舍弃 vfork() 而使用 fork()

七、进程的诞生与终止

进程的诞生
一个进程可以通过 fork() 或 vfork() 等系统调用创建一个子进程,一个新的进程就此诞生!事实上, Linux 系统下的所有进程都是由其父进程创建而来。
进程号为 1 的进程便是所有进程的父进程,通常称为 init 进程,它是 Linux 系统启动之后运行的第一个进程,它管理着系统上所有其它进程,init 进程是由内核启动,因此理论上说它没有父进程。
进程的终止
通常,进程有两种终止方式:异常终止和正常终止
进程的正常终止有多种不同的方式,譬如在 main 函数中使用 return 返回、调用 exit() 函数结束进程、 调用_exit() 或 _Exit() 函数结束进程等。
exit() 函数会比 _exit() 会多做一些事情,包括执行终止处理函数、刷新 stdio 流缓冲以及调用_exit() ,在我们的程序当中,父、子进程不应都使用 exit() 终止,只能有一个进程使用 exit()、而另一个则使用 _exit() 退出,当然一般推荐的是子进程使用 _exit() 退出、而父进程则使用 exit() 退出。其原因就在于调用 exit() 函数终止进程时会刷新进程的 stdio 缓冲区。
异常终止通常也有多种不同的方式,譬如在程序当中调用 abort() 函数异常终止进程、当进程接收到某些 信号导致异常终止等。

八、监视子进程

在很多应用程序的设计中,父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视。
1、wait()函数
系统调用 wait() 可以等待进程的任一子进程终止,同时获取子进程的终止状态信息。系统调用 wait()将执行如下动作:
⚫ 调用 wait() 函数,如果其所有子进程都还在运行,则 wait() 会一直阻塞等待,直到某一个子进程终止;
⚫ 如果进程调用 wait() ,但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那 么 wait() 将返回错误,也就是返回 -1 、并且会将 errno 设置为 ECHILD 。
⚫ 如果进程调用 wait() 之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用 wait() 也不会阻塞。wait() 函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子 进程的一些资源,俗称为子进程"收尸"。所以在调用 wait() 函数之前,已经有子进程终止了,意味着正等待着父进程为其"收尸",所以调用 wait() 将不会阻塞,而是会立即替该子进程"收尸"、处理它的"后事",然后返回到正常的程序流程中,一次 wait() 调用只能处理一次。
2、waitpid() 函数
使用 wait() 系统调用存在着一些限制,这些限制包括如下:
⚫ 如果父进程创建了多个子进程,使用 wait() 将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
⚫ 如果子进程没有终止,正在运行,那么 wait() 总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
⚫ 使用 wait() 只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。
而设计 waitpid() 则可以突破这些限制
3、 SIGCHLD 信号
⚫ 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
⚫ 当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。

相关推荐
blessing。。37 分钟前
I2C学习
linux·单片机·嵌入式硬件·嵌入式
2202_754421541 小时前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
运维&陈同学2 小时前
【zookeeper03】消息队列与微服务之zookeeper集群部署
linux·微服务·zookeeper·云原生·消息队列·云计算·java-zookeeper
周末不下雨2 小时前
win11+ubuntu22.04双系统 | 联想 24 y7000p | ubuntu 22.04 | 把ubuntu系统装到1T的移动固态硬盘上!!!
linux·运维·ubuntu
哎呦喂-ll3 小时前
Linux进阶:环境变量
linux
Rverdoser3 小时前
Linux环境开启MongoDB的安全认证
linux·安全·mongodb
PigeonGuan3 小时前
【jupyter】linux服务器怎么使用jupyter
linux·ide·jupyter
东华果汁哥4 小时前
【linux 免密登录】快速设置kafka01、kafka02、kafka03 三台机器免密登录
linux·运维·服务器
咖喱鱼蛋4 小时前
Ubuntu安装Electron环境
linux·ubuntu·electron