进程 = 内核数据结构 + 代码(只读) + 数据,所以父子进程是具有独立性的。父子进程有一个进程退出不会影响其他进程。
#include <iostream>
#include <algorithm>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int g_val = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
while (true)
{
cout << "I am child, g_val:" << g_val << ", dist:" << &g_val << endl;
sleep(1);
}
}
else if(id > 0)
{
g_val = 200;
while (true)
{
cout << "I am prant, g_val:" << g_val << ", dist:" << & g_val << endl;
sleep(1);
}
}
return 0;
}
子进程要继承父进程的数据,但是进程是具有独立性的, 当子进程需要修改数据时,需要使用写时拷贝。
写时拷贝实际上是运用了一个“引用计数”的概念来实现的,在开辟的空间中多维护了四个字节来存储引用计数。
有两种方式来存储引用计数:
当我们多开辟一个空间时,让引用计数 + 1,如果有释放空间,那么就让引用计数 - 1,但是此时不是真正的释放,是假释放,等到引用计数为0时,才是真正的释放空间。如果有修改或写的操作,那么也让原空间的引用计数-1,并且真正开辟新的空间。
struct area
{
int start;
int end;
};
struct destop
{
struct area left;
struct area right;
};
在操作系统内核中,我们可以找到:
举一个例子:我们在C语言中如果修改常量字符串,编译器会阻止你,不会使你修改常量字符串的,就是因为在页表中有一个rwx权限,常量字符串中的权限只有r,所以不能进行修改。在出现异常情况时,由于页表的存在,不会将系统内部进行破坏。
在页表中还有一个标志位:如果是0,表示进程挂起,将程序加载到磁盘中;如果是1,表示进程运行。
由于页表将父子进程的代码的权限设置为r,当OS识别出错误时,我们可以进行分情况讨论:
objdump -S xxxxx objdunp -s xxxxx > xxxxx.s // 将反汇编文件重写入.s文件中
因为fork()函数在创建子进程后,将id的值发生改变,那么我们将会发生写时拷贝,就会使id的值返回两份。
进程 = 内核数据结构 + 自己的代码和数据,我们在申请内存空间时,我们一般是如何进行申请的?
操作系统这样做的好处是:
- 充分保证内存的使用率,不会空转
- 提升了new或者malloc的速度
Linux系统中每一个CPU中拥有一个运行队列,如果有多个CPU就要考虑进程个数的父子均衡问题。Linux系统是分时操作系统,讲究公平。
queue数组的下标说明:
在之前的优先级中讲过nice值的取值范围是:-20~19,而这里的普通优先级下标正好也是40个。在Linux系统中,我们的进程是普通的优先级,所以一一对应queue数组的100~139。
而还有一个系统:实时操作系统。实时优先级对应实时进程,实时进程是指先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所以对于queue当中下标为0~99的元素我们不关心。
由于队列的数组元素过多,我们可以使用位图来判定哪个优先级对应有进程。因为有140的元素,所以我们可以使用5个整形数组的元素可以将整个元素表示出来。我们可以通过数字的二进制来发现哪个优先级上有进程运行,搜寻次数大大减少。
队列中的三个元素:nr_active、bitmap[5]、queue[140]。
- nr_active:代表总共有多少个运行状态的进程;
- bitmap[5]:位图,表示哪个优先级上有运行状态的进程;
- queue[140]:表示的是优先级,分为普通优先级和实时优先级。
时间片还没有结束的所有进程都按照优先级放在活动队列当中,相同优先级的进程按照FIFO规则进程排队调度。
调度过程如下:
当活动队列上的进程随着时间片的轮转,数量会越来越少;而过期队列的进程会越来越多;直到时间片到期,活动队列的进程为0,过期队列的进程数量最大。此时,操作系统只需交换一下active指针和expired指针,将指针内容进行交换,继续进行时间片的轮转即可,如此循环往复。