进程控制详解
进程创建
fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h> pid_t fork(void); 返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
- 当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
int main( void ) { pid_t pid; printf("Before: pid is %d\n", getpid()); if ( (pid=fork()) == -1 ) perror("fork()"),exit(1); printf("After :pid is %d, fork return %d\n", getpid(), pid); sleep(1); return 0; } 运行结果: [root@localhost linux]# ./a.out Before: pid is 43676 After:pid is 43676, fork return 43677 After:pid is 43677, fork return 0
这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示:
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器
决定。
fork函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid。
- 失败返回 -1
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
注意:
在进程创建中就是:子进程复制了父进程中的大部分的信息,因此子进程有自己的变量,但是自己的变量经过页表映射后与父进程访问的是同一块物理内存, 当这块内存空间中的数据即将要修改(不管是子进程发生改变还是父进程发生改变,都会给子进程创建一块新的空间),则给子进程重新开辟内存空间,并拷贝数据过去写时拷贝技术:主要是提升子进程创建效率,避免不必要的内存消耗。(意思就是一开始父子进程访问同一块物理内存,没有给子进程创建独立的物理内存空间,当有一方发生改变之后,会给子进程创建独立的物理内存,这样就防止了子进程创建之后没有使用导致的资源浪费)
我们经常使用的malloc动态申请一块空间——其实只是先分配了 一个虚拟地址(物理内存并没有直接被开辟),当第一次要修改空间数据的时候才会被分配。
vfork函数 和 fork函数的简单区别
pid_ _t vfork(void)–创建一个子进程(在fork实现了写时拷贝技术之后,用的就很少了) ,创建一个子进程出来,父子进程共用同一个虚拟地址空间,“create a child process and block parent”——创建一个子进程并阻塞父进程,直到自己成exit退出,或程序替换之后,父进程再开始运行。
fork创建子进程之后,父子进程谁先运行不一定, 看系统调度但是vfork创建子进程,一定是子进程先运行, 只有子进程退出或者程序替换之后父进程才会继续运行。
父子进程共用同-一个虚拟地址空间,则意味着用的是同一个栈如果同时运行就会导致栈混乱,因此先阻塞父进程,直到子进程退出了(所有函数都出栈了)或者程序替换(意味着重新开辟了自己的地址空间,有了自己的栈),父进程才会运行
进程终止
进程退出场景
- 任务完美完成,正常退出
- 任务没有完成,正常退出
- 异常退出
进程常见退出方法
退出:(如何终止一个进程)
1 在main函数中return。 仅在main函数中使用是退出程序运行
return是终止一个函数,并返回- -个数据; main函数是程序的入口函数, 入口函数一旦退出,程序运行就会终止。
2.库函数: void exit(int retval) 在任意位置调用,都可以退出程序运行
系统调用接口是操作系统向.上层提供的用于访问内核的接口,功能相对都比较单一,大佬们针对典型场景。对系统调用接口进行封装,封装出了适用于典型场景库函数。
3.系统调用接口: void_ exit(int retval); 在任意位置调用,都可以退出程序运行
exit和_ exit的区别在于退出程序运行前,是否会将缓冲区中的数据进行刷新写入文件中。
退出程序有多种方式,在合适的场景选择合适的方式进行即可。
正常终止(可以通过 echo $? 查看进程退出码):
- 从main返回
- 调用exit
- _exit
异常退出:
-ctrl + c,信号终止
_exit函数
#include <unistd.h> void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值 是255。
exit函数
#include <unistd.h>
void exit(int status);
exit最后也会调用_exit, 但在调用exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
如以下代码示范:
int main() { printf("hello"); exit(0); } 运行结果: [root@localhost linux]# ./a.out hello[root@localhost linux]# int main() { printf("hello"); _exit(0); } 运行结果: [root@localhost linux]# ./a.out [root@localhost linux]#
这段代码就说明,使用exit函数会刷新缓冲区,而使用_exit的话就不会刷新缓冲区, 所以上述代码中_exit退出后不会进行打印。修改方法:在_exit的printf中加上“\n”即可。