进程创建
fork函数
调用fork函数让正在运行的进程创建出来一个子进程。得到的新进程为子进程,而原进程为父进程。
pid_t fork(void)
fork函数有两个返回值,如果创建成功,那么返回给父进程一个大于0的值,即子进程的进程号,返回给子进程数字0,如果创建失败,那就给父进程返回-1,父子进程是独立的进程,fork分别在父子进程当中进行返回,由于父子进程返回不同的值,且相互独立,我们可以通过返回值来让父子进程执行不同的代码。
我们来简单看一下正在运行的父进程创建子进程的代码,试验一下:
运行结果:
观察数字,我们可以给出一个经验,一般来说,子进程的pid比父进程小1。
fork内部完成的事情:
- 创建子进程,子进程拷贝父进程的PCB
- 分配新的内存块和内核数据结构(task_struct)给子进程
- 将父进程部分数据结构内容拷贝给子进程
- 添加子进程到系统进程列表当中,添加到双向链表里当中
- fork返回,操作系统开始进行调度,调度的时候遵循以下原则:先来先服务、短作业优先、优先级优先、时间片轮转等
用户空间、内核空间
内核空间:Linux操作系统和驱动程序运行在内核空间。系统调用的函数都是运行在内核空间的,因为是操作系统提供的函数。
用户空间:应用程序都是运行在用户空间的,应用程序属于程序员自己写的代码,程序员自己写的代码都是运行在用户空间的。
当程序员写的代码中调用了系统调用函数,就会从用户空间切换到内核空间去执行系统调用函数,执行完之后再次回到用户空间继续执行程序员写的代码。
写实拷贝
fork创建子进程的时候,子进程会拷贝父进程的PCB,页表也会拷贝父进程的。所以同一个变量的的虚拟地址和物理地址的映射关系在父进程和子进程中是一样的,也就是说,操作系统并没有给子进程当中的变量在物理内存当中分配内存空间进行存储,子进程的变量还是父进程的物理地址当中的内容。
当子进程中的变量发生改变时,才会以写实拷贝的方式进行拷贝,也就是分配物理内存,此时父子进程通过各自的页表,指向不同的物理地址。
当子进程中的变量不发生改变时,父子进程共享一个数据,这样子有一个明显的好处就是节省内存空间。
执行结果:
fork创建子进程时的一些特性
- (进程独立性)父子进程相互独立,互不干扰。各自有各自的进程虚拟地址空间和映射页表,不能访问对方的数据
- 父子进程独立被操作系统调度,父子进程是抢占式执行,父子进程先后执行顺序本质是由操作系统调度决定的,不过进程自身情况也会影响执行先后顺序
- 子进程是从fork之后开始运行
- 代码共享,数据独有
守护进程
父进程创建子进程,通过进程程序替换,让子进程执行真正的业务,父进程负责守护子进程,子进程和父进程之间进行进程间通信,当父进程检测到子进程异常的时候,父进程就会重新启动子进程(再创建一个子进程),让子进程继续提供服务。
守护进程是提高“高可用”的一种手段。
进程终止
进程终止有两种场景,正常终止和异常终止。
正常终止又有两种情况:代码运行完毕,完成了既定的代码功能;代码运行完毕,但是没有完成既定的功能。
正常终止
可以通过命令“echo $?”查看进程退出码
正常终止有下面三种情况:
我们查看一下进程退出码:
这种情况下,进程退出码就是return返回的值,我们修改一下return后面的值,就会发现进程退出码也会改变。
查看一下进程退出码:
调用exit函数之后,就会直接退出进程了,所以上面的代码查看进程退出码就是2不是1了,因为后面的打印和return 0 都不会执行。
异常终止
- 1、解引用空指针,解引用野指针(垂悬指针)
执行结果:
产生了段错误,进程异常终止,此时会产生一个核心转储文件,我们来看一看:
看不到核心转储文件,这是为什么呢?因为core file size的-c的位置是0,有限制,我们修改一下:
现在就能产生核心转储文件了,再执行一次,就能看到产生的核心转储文件了。
我们看一下这个coredump文件的这个段错误的信息:
- 2、double free
对一块空间释放两次,会引起进程的异常终止,也会产生coredump文件。
执行产生了错误:
进程内存访问越界的时候,会被操作系统强杀。
exit和_exit的区别
exit函数_exit函数两者最大的区别就是exit函数会比_exit函数多了两个步骤:执行用户定义的清理函数、冲刷缓冲,关闭流等。
- 1、执行用户自定义的清理函数
void (*function)(void) 这是一个函数指针
指针函数:本质是一个函数,返回值是指针,也就是说,指针函数就是返回值为指针的函数。
函数指针:本质是一个指针,指向代码段中函数入口地址,(*function) 是一个整体,代表的是指向该函数的指针,(function) 是函数指针变量,函数名就是函数的入口地址,所以函数指针变量就是函数名。
atexit是注册函数指针保存的函数地址到内核当中去,注册这个函数并不是调用这个函数。所以下面这段代码中,atexit注册了atexit_callback函数,但是并没有调用执行,而是在整个进程结束之后,进行回调,所以才有了这样子的执行结果。
4 void atexit_callback(){
5 printf("I am atexit_callback,hhhh.\n");
6 }
7 int main(){
8 atexit(atexit_callback);
9 printf("hello world.\n");
10 return 0;
11 }
执行结果:
当程序达到了某种固定的场景的时候,会进行回调,调用的函数被称为回调函数。
printf("hello world.\n");
“hello world”并不是直接被打印到屏幕上面的,而是先被放入缓冲区,再刷新缓冲区到屏幕上。
换行符可以刷新缓冲区、从main函数的return返回可以刷新缓冲区、exit函数、fflush函数(强制刷新缓冲区)这几种方式都可以刷新缓冲区。
#include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main(){
5 printf("hello world");
6 fflush(stdout);
7 _exit(2);
8 int count = 5;
9 while(count){
10 sleep(1);
11 count--;
12 }
13 return 0;
14 }
缓冲方式
- 全缓冲:缓冲区写满再进行IO
- 行缓冲:输入和输出遇到换行符的时候,标准I/O库执行I/O操作,也就是说,一行写满就进行刷新。
- 不缓冲:不带缓冲,标准I/O库不对字符进行缓冲存储,也就是说放入缓冲区后立即就刷新出来。