本教程操作环境:linux7.3系统、Dell G3电脑。
Linux系统种创建进程有fork、vfork、clone这个三名命令可供使用。
(资料图片仅供参考)
fork
fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源。例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是复制。这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipe,共享内存等机制, 另外通过fork创建子进程,需要将上面描述的每种资源都复制一个副本。这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个可执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程。但由于现在Linux中是采取了copy-on-write(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。所以有了写时复制后呢,vfork其实现意义就不大了。
fork()调用执行一次返回两个值,对于父进程,fork函数返回子程序的进程号,而对于子程序,fork函数则返回零,这就是一个函数返回两次的本质。
在fork之后,子进程和父进程都会继续执行fork调用之后的指令。子进程是父进程的副本。它将获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段。正文段存放着由cpu执行的机器指令,通常是read-only的。
vfork
vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。
因此,上面的例子如果改用vfork()的话,那么两次打印a,b的值是相同的,所在地址也是相同的。
但此处有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。
Vfork也是在父进程中返回子进程的进程号,在子进程中返回0。
用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之。)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。
clone
系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags来决定。另外,clone()返回的是子进程的pid。
下面详细了解fork命令(进程创建)。
深入 fork 函数
在 Linux 中 fork 函数是一个非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
fork 函数的返回值:
给父进程返回子进程的 pid给子进程返回0接下来我们举例使用一下fork函数 ()
我们编译,然后运行一下:
fork 的常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。fork调用失败的原因
系统中有太多的进程实际用户的进程数超过了限制在重温了一下 fork 函数的使用后,接下来我们来研究一个话题:
fork() 创建子进程,操作系统做了哪些操作?
进程调用 fork,当控制转移到内核中的fork代码后,内核做了以下操作:
分配新得内存块和内核数据结构给子进程。
将父进程部分数据结构内容拷贝至子进程。
添加子进程到系统进程列表中。
fork返回,开始调度器调度。
父进程执行完 fork之前的代码(before)后,调用 fork 创建子进程,父子两个执行流分别执行。注意:fork 之后,谁先执行完全由调度器决定。
这里还有一个问题,当fork之后,父子进程代码共享是 after 共享,还是所有代码都进行共享?为什么子进程总准确地执行 fork 之后对应的代码?
答案: 所有代码共享,因为CPU记录了进程的执行位置。
代码进行汇编之后,会有很多行代码,而且每行代码加载到内存之后,都有对应的地址。因为进程随时都可能被中断(可能并没有执行完),下次继续执行时,还必须从之前的位置继续运行(并不是程序最开始或main函数处),这就要求 CPU 必须实时记录下当前进程执行的位置。所以,CPU内有对应的寄存器数据,用来记录当前进程的执行位置,此寄存器叫做EIP,也称作为pc(point code 程序计数器),用来记录正在执行代码的下一行代码的地址(上下文数据)。当子进程创建时,会修改其EIP。此时子进程便会认为EIP的中保存下的数据,就是要执行的代码。创建子进程时,操作系统给子进程分配对应的数据结构,子进程独立运行,因为进程具有独立性。
理论上,子进程也要有自己的代码和数据,但是一般而言,创建子进程没有加载的过程,子进程本身并没有自己的代码和数据。
所以,子进程只能 "使用" 父进程的代码和数据,而代码是只读的,父子共享不会冲突;而数据是可能被修改的,必须进行分离。
这时,操作系统便采用写时拷贝的策略。
写时拷贝
OS 为何采用写时拷贝技术,对父子进程进行分离
写入时再进行拷贝,是高效使用内存的一种表现。
提高了系统的运行效率。
OS无法在代码执行前预知哪些空间会被访问。
扩展知识:进程终止和
进程终止
当进程终止时,操作系统释放了进程申请的相关内核数据结构和对应的代码的数据,其本质就是释放系统资源,
1、进程退出码
进程终止的常见方式:
代码跑完,结果正确。代码跑完,结果不正确。代码没有跑完,程序崩溃了。区分第一种情况和第二种情况我们可以通过进程的退出码很清晰的辨别。
关于学习的 C语言中 main 函数的返回值,其中 main 函数的返回值就是进程的退出码。其意义是返回给上一级进程,用来评判该进程执行结果。
现在我们编写一个简单的 C 程序。
然后我们可以通过echo $? 获取最近一个进程的退出码。
进程的返回值有 0 和非0两种情况,其中 0 表示程序成功运行并结果正确,而非0表示成功运行但结果有误,非零值有无数个,不同的非零值就可以就表示着不同的错误,方便我们定义错误的原因。
那常见的错误信息有哪些呢?
我们可以使用 strerror 将其打印出来
结果如下:
发现 linux 下,共有133条错误码
当然,程序崩溃的时候,退出码没有意义。
众所周知,Linux 是用C语言写的,其中命令本质就是C语言程序,所以我们可以简单的拿 ls 命令来举例
而 2 号退出码对应的报错信息:
2、exit 与 _exit
关于终止一个进程可以使用 return 语句,还可以调用 exit 和 _exit 函数
exit函数:
_exit函数:
关于这两个函数的区别有很多,我们先举一个小例:
我们接下来使用 printf 打印一条信息,然后sleep三秒,再使用 exit 退出,并观察结果
因为我们带上了 \n ,加上 \n 会刷新缓冲区,屏幕上即出现我们打印的内容。
如果我们不带上 \n ,我们再观察结果:
发现:因为没有 \n ,所以 printf 中的内容并没有在休眠前被打印出来,而是调用 exit 后将缓冲区的内容刷新出来输出在屏幕上。
接下来我们使用 _exit 函数
运行可执行文件 b:
发现什么内容都没有被打印出来,使用 echo $? 打印最近进程退出码,发现 b 文件确实被执行了。
这说明,exit是库函数,而_exit 是系统调用,其退出进程时并没有刷新缓冲区中的内容。
此时我们便能得出一个结论:
printf 数据是保存在"缓冲区"中的,exit可以将其刷新,而系统调用接口_exit不能将其刷新。所以,缓冲区必定不在操作系统内部,而是由C标准库维护的。
相关推荐:《Linux视频教程》
以上就是linux 创建进程命令是什么的详细内容,更多请关注php中文网其它相关文章!