文章目录
一、操作系统概念理解
冯-诺依曼结构在外设和CPU之间放了一个内存作为中间物,以此来提高整体运行的速度。除此之外,内存还有别的用处,即使没有调用数据的要求,外设也可以先把数据放到内存中进行预加载,来减轻CPU负担。当然,如果不使用,内存也可以释放数据,还给外设。
了解这些后,接下来就是操作系统。这时应有一个细致的提问
为什么要有操作系统?
上一篇讲到计算机要处理数据,需要一定的规则。这也就是操作系统对于数据的安排原理:先描述,再组织。操作系统会把所有数据构建成对象,填充值,并以某种数据结构来管理起来,比如链表,之后再以结构建模,对于数据的管理就变成了对结构的管理,最后给用户更好的服务。
只有硬件无法起作用,所以操作系统就是为了计算机给人服务而出现的。上面所说的到冯-诺依曼结构也体现了操作系统的作用,内存如何知道数据应当还到哪里?外设怎么确定把数据给内存是为了预加载?这里还有更多细致的问题,这些问题也都可以去找操作系统来解答。
虽然我们了解了操作系统,但操作系统可不了解我们。虽然它给人提供服务,但不相信人类。操作系统为了自己的隐私,做了很多的保险,比如给用户提供接口来使用功能,也就是系统调用,不给用户开权限看系统的代码等等,在这基础之上,操作系统也衍生出了更多功能,以往所写的C语言代码是跨平台的,如果是使用系统接口来实现代码的,那就只能在这个系统上才能运行。
像平常所使用的ls指令,查看文件,文件的各项属性此时已经在磁盘里存储着,使用ls后,系统就会使用相应的系统接口来传达指令,到达磁盘部分,磁盘再往上返回用户要求的数据。
系统是一个体系,在这个体系下,每个细节都有规则。整个结构像一个层状结构,必须一层一层走,不能跳过某一环节。
总结一下,
先描述,再组织。
用结构体来描述数据,用链表或者其它数据结构来组织数据。
操作系统给到用户的接口,也就是系统调用,为了方便,把某些相关联的接口封装起来,形成库。计算机语言有自己的库和编译器,说明语言也是在系统规则之上建立起来的,语言也有系统接口,用这些接口来调用系统的功能。有的库文件由系统接口,有的则无,只要用到系统功能那就会封装进系统接口。库文件之所以封装接口也是为了程序员更方便地使用功能。
二、进程的基本理解
1、什么是进程?
在系统形成一个可执行文件时,或者我们touch了一个文件,文件都会有内容+属性。文件被加载进内存时,它的代码和数据都会一并加入,系统以此来运行它们。但是这样还不算进程。之前提到过,系统对于数据的管理方式是先描述再组织,进入内存加载的这些文件又是如何管理的?文件在被加载进内存时,操作系统内核就会创建一个数据结构,叫做pcb,在Linux中则是有task_struct结构体。pcb会把进程的属性拿过来储存,并有指针指向这个已经存在的进程。更多的程序进入内存后,系统会创建更多pcb,一一对应着存入进程的属性;为了更好地管理,每个pcb之间也有指针相连,这样就形成了一整个链表。用户想要结束某一进程时,系统就遍历链表,找到对应的pcb,释放掉即可;用户想要执行某一个程序时,就把对应pcb的属性以及程序的代码和数据拿到cpu里运算。这样,对于进程的管理就变成了对数据结构的管理,这也就是系统对进程建模的过程。
所以进程我们可以理解为 内核关于进程的相关数据结构+进程的代码和数据
task_struct是pcb的一种,Linux的task_struct是以双链表形式来管理进程的。pcb拿到的属性包括标示符,状态,优先级,程序计数器,内存指针,上下文数据,I / O 状态信息,记账信息等等。
2、进程的属性
1、指令查看进程
pcb里有进程的属性,这个属性和文件的属性有关系但不多。pcb是一个内核的数据结构,和磁盘内的文件没有太大的关系,但也需要知道执行的是哪个程序。
现在实际操作一下,做一个文件。
#include <unistd.h>
#include <stdio.h>
int main()
{
while (1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
此时查看一下进程
ps axj | grep myprocess
只查看myprocess这个程序的进程。
为了更好的观察,我们把进程第一行拿出来,看看都是什么信息,以及使用逻辑与,把进程信息也一并显示出来
ps axj | head -1 && ps axj | grep myprocess
如果多次执行同样的程序,系统会开多个进程,这时候看进程属性会发现它们都不一样
grep那一行可以不用管,想去掉的话后面再加一个管道即可。
ps axj | head -1 && ps axj | grep myprocess | grep -v grep
2、目录查看进程
除了指令方式,还可以通过查看根目录的proc目录来查看当前进程
proc目录保存了进程的属性,proc是一个内存级的目录,只在程序运行时才会出现。
图中有很多蓝色数字的文件,这些数字代表存在的进程的PID,我们查看一下刚才两个进程中的其中一个。
cd /proc/11644
里面就是这个进程的所有属性,比如这两个显而易见的
如果这时候结束掉这个进程,那么我们就无法查看这个文件了,或者说这个文件也不存在了,内容已经被删除了。
怎样查看PID
为了更方便的查看PID,而不是写一长串代码,我们可以在文件里打印出PID。getpid(),头文件是unistd.h 和 sys/types.h,用man指令查看即可。
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main()
{
while (1)
{
printf("hello world\n");
printf("PID: %d\n", getpid());
sleep(1);
}
return 0;
}
效果就是这样
3、进程与进程之间
1、父子进程概念
进程与进程之间存在关系,比如常见的父子关系。getppid可以查看父进程的pid。添加进文件后,还是刚才的程序,无论怎样结束进程又重新执行,进程pid会变,但是父进程pid不变。
查看一下父进程
ps axj | head -1 && ps axj | grep 16710
我们可以看到一个bash,也就是每次登录后形成的命令行解释器,这个本质上也是一个进程。对于用命令行启动的程序,都会成为一个进程,父进程都是bush。可是为什么父进程会是bash?bash要查看代码是否有错误,如果有错,那么bash就挂了,所以bash创建子进程来监管整在这里插入代码片
个代码的运行,防止自己挂掉。
我们可以自行杀掉bash。除了Ctrl + c可以退出进程外,kill -9也可以杀掉进程,不管是不是父进程。杀掉了bash,指令就无法正常使用了,这时候只能重启Xshell。
2、创建子进程—fork的基础使用方法
fork指令
fork可以用来创建子进程。
以这个代码为例
printf("asssssssssssssssssssss\n");
fork();
printf("dddddddddddddddddddddd\n");
sleep(1);
打印出来的结果就是as打印了一次,d打印了两次。我们修改一下代码
printf("dddddddddddddddddddddd: pid : %d, 父进程pid: %d\n", getpid(), getppid());
pid不一样,父进程pid也不一样,但是第二个ppid和第一个pid是一样的。所以这里就利用pid创建了一个子进程。
现在我们在as后面也打印pid。
这里as的父进程就是bash,d的父进程也是bash,不过后面又利用d创建了一个子进程。
既然能创建子进程,那么进程之间又如何控制呢?
fork函数里有这么一个描述。
成功创建父进程后,子进程的pid返回给父进程,0返回给子进程。如果失败,-1会返回给父进程。
我们先看代码。
int main()
{
printf("asssssssssssss: pid: %d, 父进程pid: %d\n", getpid(), getppid());
pid_t ret = fork();
printf("dddddddddddddd: pid: %d, 父进程pid: %d, ret: %d, &ret: %p\n", getpid(), getppid(), ret, &ret);
sleep(1);
return 0;
}
我们可以看到19941是下面子进程的pid,子进程也得到了0。
不过呢一般我们不会这么用fork。
pid_t ret = fork();
assert(ret != -1);
if(ret == 0)
{
//子进程
while(1)
{
printf("子进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else if(ret > 0)
{
//父进程
while(1)
{
printf("父进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
代码可以正常执行,即使有if和else if,说明这是两个执行流,都可以运行。
这里就体现出了fork的规则
1、fork之后,执行流会变成若干个2个执行流
2、fork之后,运行顺序由调度器决定
3、fork之后的代码共享,通常我们通过if和else if来进行执行流分流
我们会发现fork为什么可以有两个返回值,为什么if和else if都能执行?接下来我们要解决fork的一些疑问。
3、fork原理的初级理解
1、fork的操作
在写入代码和数据后,内核会创建pcb数据结构,这是一个父进程,fork会相应地建立一个子进程,父进程的大部分属性会拷贝到子进程,比如pid,ppid,父子进程就不一样,所以子进程的属性是以父进程属性为模板创建的。父进程指向对应的代码和数据,子进程也会指向它们。
2、fork如何看待代码和数据
进程与进程之间是相互独立运行的,父子进程也一样。上面的代码实际运行时,我们杀掉父进程,代码还会继续运行,子进程并不受影响。虽然事实是这样,但是仍然有问题。虽然进程互相独立,但是指向的代码和数据是一样的,这如何保证父子进程不受影响?
两个层面来看
代码:虽然指向同样的代码,但本质上程序员写出来的都是给父进程的代码,父进程分出一个子进程去读子进程自己的代码;况且代码有一个特性,只读。在编译器运行过程中,代码是不会被改动的,除非退出程序,我们再去修改,所以代码方面两者不会受影响。
数据:还是上面那些代码,先建立一个变量,给上固定的值,然后父子进程两个while里的printf括号里都打印上变量的值和地址,在其中一个进程printf后给这个变量重新赋值,比如子进程里,最终的结果就是父子进程会打印出不同的数值,不过地址都一样,所以也可以发现数据不受影响,这是因为当有一个执行流尝试修改数据的时候,系统会自动给当前进程触发写时拷贝,会另给一个空间去做改动。写时拷贝先不管,之后再详细说,只知道系统做到了什么就可以了。
3、fork如何看待两个返回值问题
在函数内部准备执行return的时候,函数的主体功能已经完成。对于系统来说,fork本质上是一个函数,fork创建完子进程pcb后,就只剩了return了,但是return是一个语句,也就是一个代码,父进程执行完前面的也会来到这里,所以return是被父子进程各自调用了一遍,也就出现了2个返回值的情况。
在fork前面我们定义了一个变量来接收值,但是一个变量真的就出现了两个值?return的时候,把值传给了外面接收用的变量,由于这个变量是个父进程的变量,传过来的时候系统就触发了写时拷贝,虽然我们地址都一样,但是实际还是放到了不同的位置。
本篇只是简单写了一些fork的知识,fork还有更复杂的知识,以及对于Linux、进程概念的理解还有更深层次的,本篇只是一个基础知识。
结束。