Linux学习记录——구 进程概念的基础理解

文章介绍了操作系统的基本概念,强调其在数据管理和协调硬件中的作用。接着,详细阐述了进程的概念,包括进程的属性、如何查看进程以及进程间的父子关系。重点讨论了fork函数在创建子进程中的工作原理,提到了代码和数据的独立性以及两个返回值的问题。
摘要由CSDN通过智能技术生成


一、操作系统概念理解

冯-诺依曼结构在外设和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、进程概念的理解还有更深层次的,本篇只是一个基础知识。

结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值