Linux系统编程---进程的概念

一、 冯诺依曼体系结构。

       输入设备,存储器,输出设备,运算器,控制器。数据总线,控制总线,系统总线。

二、一个程序要运行,必须先加载到内存中,为什么?

        因为冯诺依曼体系结构所确定。

三、操作系统。

        是什么?

        操作系统是一款管理软硬件资源的软件!

        为什么?

        1、操作系统帮助用户,管理好下面的软硬件资源!

        2、为了给用户提供一个良好(稳定,高效,安全)的运行环境。

        操作系统通过管理好底层的软硬件资源(手段),为用户提供一个良好的执行环境(目的)

        用户:普通用户,程序员,普通用户使用程序员开发出来的软件。

        操作系统里面会有各种数据,可是,操作系统不相信任何用户!操作系统为了保证自己数据安全,也为了保证给用户提供服务,操作系统以接口的方式给用户提供调用的入口,来获取操作系统内部的数据。接口是操作系统提供的用C实现的,自己内部的函数调用 --- 系统调用。所有访问操作系统的行为,都只能通过系统调用完成!

        怎么办? 如何管理?

        1、管理者和被管理者是不需要见面的。

        2、管理者在不见面的情况下,如何做好管理呢?

       只要能够得到管理信息,就可以在未来进行管理决策 --- 管理的本质是通过对数据的管理,达到对人的管理。

        3、管理者和被管理者面都不见,我们怎么拿到对应的数据呢?

        通过执行者。

        管理者就是操作系统,执行者就是软硬件资源,

   在操作系统中,管理任何对象,最终都可以转化成为对某种数据结构的增删查改

        先描述,再组织就是我们的管理方法。

        操作系统中一定存在各种各样的数据结构。

四、进程的概念

        加载到内存的一段程序成为进程。正在运行的程序也是进程。

        ps ajx | grep myprocess

理解:

一个操作系统,不仅仅只能运行一个进程,也可以同时运行多个进程,操作系统必须将进程管理起来。先描述,再组织。任何一个进程,在加载到内存的时候,形成真正的进程时,操作系统,要先创建描述进程的结构体对象 ----- PCB,process ctrl block ----- 进程控制块。想一想,人是通过什么来认识一个事物的,答案是都是通过属性认识的,当属性够多的是时候,这一堆属性的集合,就是目标对象。那么PCB里面包含的就是进程属性的集合。由于操作系统是由C语言编写的,所以使用结构体来描述这些属性。进程可能有进程编号、进程的状态、进程的优先级、相关的指针信息等属性。根据进程的PCB来为进程创建PCB对象。所以:进程 = 内核PCB数据结构对象 + 你自己的代码和数据到此我们完成了描述阶段的任务。在操作系统中,对进程进行管理,变成了对单链表进行增删查改!到此我们完成了组织阶段的任务。

那么Linux中,是怎么做的呢?

在Linux中的PCB是task_struct结构体

内容分类:

标识符:描述本进程的唯一标识符,用来区别其他进程。状态,优先级,程序计数器,内存指针,上下文数据,I/O状态信息,记账信息,其他信息等。这里查看源码可以看到

struct task_struct {
    ......
    /* 进程状态 */
    volatile long state;
    /* 指向内核栈 */
    void *stack;
    /* 用于加入进程链表 */
    struct list_head tasks;
    ......
    /* 指向该进程的内存区描述符 */
    struct mm_struct *mm, *active_mm;
    ........
    /* 进程ID,每个进程(线程)的PID都不同 */
    pid_t pid;
    /* 线程组ID,同一个线程组拥有相同的pid,与领头线程(该组中第一个轻量级进程)pid一致,保存在tgid中,线程组领头线程的pid和tgid相同 */
    pid_t tgid;
    /* 用于连接到PID、TGID、PGRP、SESSION哈希表 */
    struct pid_link pids[PIDTYPE_MAX];
    ........
    /* 指向创建其的父进程,如果其父进程不存在,则指向init进程 */
    struct task_struct __rcu *real_parent;
    /* 指向当前的父进程,通常与real_parent一致 */
    struct task_struct __rcu *parent;

    /* 子进程链表 */
    struct list_head children;
    /* 兄弟进程链表 */
    struct list_head sibling;
    /* 线程组领头线程指针 */
    struct task_struct *group_leader;

    /* 在进程切换时保存硬件上下文(硬件上下文一共保存在2个地方: thread_struct(保存大部分CPU寄存器值,包括内核态堆栈栈顶地址和IO许可权限位),内核栈(保存eax,ebx,ecx,edx等通用寄存器值)) */
    struct thread_struct thread;

    /* 当前目录 */
    struct fs_struct *fs;

    /* 指向文件描述符,该进程所有打开的文件会在这里面的一个指针数组里 */
    struct files_struct *files;
    ........
  /* 信号描述符,用于跟踪共享挂起信号队列,被属于同一线程组的所有进程共享,也就是同一线程组的线程此指针指向同一个信号描述符 */
  struct signal_struct *signal;
  /* 信号处理函数描述符 */
  struct sighand_struct *sighand;

  /* sigset_t是一个位数组,每种信号对应一个位,linux中信号最大数是64
   * blocked: 被阻塞信号掩码
   * real_blocked: 被阻塞信号的临时掩码
   */
  sigset_t blocked, real_blocked;
  sigset_t saved_sigmask;    /* restored if set_restore_sigmask() was used */
  /* 私有挂起信号队列 */
  struct sigpending pending;
    ........
} 作者:补给站Linux内核 https://www.bilibili.com/read/cv15744437/ 出处:bilibili

Linux内核中,最基本的组织进程task_struct的方式,采用双向链表组织的。

查看进程属性的方法:

查看 ls/proc 目录,开机的时候有,关机的时候就不在了

里面也是目录,都是通过进程ID来标识的。注意PID标识符是不确定的。

其中cwd就是当前的工作目录,current work dir:当前进程的工作目录

标识符:

杀掉进程 

那么如何拿自己的pid呢?利用系统调用getpid(),pid是唯一的

一个监控的小脚本

获得父进程的pid叫做ppid

我们可以发现父进程ID是不变的,21823进程号是谁呢?

它是bash进程,每次登录后,它的进程号也会变化

自己创建进程,运行期间创建子进程,使用fork函数

fork的返回值:

成功时对父进程返回子进程的pid,给子进程返回0

#include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 
  5 int main()
  6 {
  7   printf("我是父进程,pid: %d,ppid:%d",getpid(),getppid());
  8 
  9   pid_t id = fork();
 10   if(id == 0)
 11   {
 12     //子进程
 13     while(1)
 14     {
 15        printf("我是子进程,pid: %d,ppid:%d\n",getpid(),getppid());
 16        sleep(1);
 17     }
 18   }
 19   else if(id > 0 )
 20   {
 21     while(1)
 22     {                                                                                                                                                                                                        
 23 
 24        printf("我是父进程,pid: %d,ppid:%d\n",getpid(),getppid());
 25        sleep(1);
 26     }
 27 
 28   }
 29   else
 30   {
 31     printf("执行错误\n");
 32   }
 33 
 34   return 0;
 35 
 36 }

问题来了这里的if else都满足了,这是为什么呢,下面解释

首先我们提出四个问题:

1、为什么fork要给子进程返回0,给父进程返回子进程pid

返回不同的返回值,是为了区分让不同的执行流执行不同的代码块,一般而言fork之后的代码父子共享,给父进程返回子进程pid是为了让父进程标识子进程,在为了我们可能创建多个子进程,那么这些子进程都得交由父进程进行管理,那么父进程怎么区分他们呢,这就需要用到fork给父进程返回子进程的pid。

2、一个函数是如何做到返回两次的?如何理解?

fork也是一个函数,pid_t fork(void)它有对应的实现代码块,走到return语句时,它的主要工作就已经完成了。创建子进程,(创建子进程PCB,填充PCB对应的内容,让子进程和父进程指向同样的代码,父子进程都有独立的task_struct,可以被CPU调度执行了。)return是代码,属于父子共享的所以就被返回了两次

3、一个变量怎么会有不同的内容,如何理解?

在任何平台,进程在运行的时候,是具有独立性的。因为数据可能被修改,不能让父子进程共享同一份数据!操作系统必须把父进程的数据拷贝一份给子进程,为了避免浪费系统资源,可以进行数据层面的写时拷贝,返回时是写入id所以要发生写时拷贝,那么怎么实现用一个id就可以实现这种方法呢?需要在进程地址空间里进行讲解。

4、fork函数,究竟干了什么,如何理解?

进程 = 内核数据结构 + 代码和数据

创建子进程:系统中多了一个进程,那么系统里面就多了一个task_struct结构体,fork之后,父子进程代码共享,运行时代码是不可以修改的。我们为什么要创建子进程呢?为了让父和子执行不同的事情,需要想办法让父和子执行不同的代码块。让fork具有不同的返回值。

如果父子进程被创建好,fork()之后,父子进程谁先运行呢?谁先运行,由调度器来确定,我们时左右不了的。这里可以参考进程调度算法。

bash如何创建子进程?调用fork(),让子进程执行解释新的命令。

进程的状态

1、一般的操作系统学科对进程状态的解释,进程状态,运行,阻塞,挂起

运行时队列

在运行队列中的进程,称为运行状态的进程。 表示我已经准备好了,可以随时被调度

一个进程只要把自己放到CPU上开始运行了,不是一直要执行完毕,才把自己放下来。每一个进程都有一个时间片的概念,它只会在时间片内才会在CPU上执行。在一个时间段内,所有的进程代码都会被执行,并发执行。大量的把进程从CPU上放上去,拿下来的动作成为进程切换。

阻塞状态

操作系统对设备的管理,先描述,再组织。

在等待某个资源就绪时,该进程的PCB会放到对应设备的阻塞队列中,该进程的状态就变成了阻塞状态。就绪后把阻塞队列中的进程PCB放到运行队列中。

挂起状态:

在等待时,操作系统内部的内存资源严重不足了,在保证正常的情况下,省出来对应的内存资源,会把进程的代码和数据交换到磁盘中,内存充足后换入到内存中。磁盘中的交换分区就是干这个事的磁盘空间。

2、具体的Linux状态是如何维护的

Linux中具体的状态有以下几种

R 运行态

S 睡眠态也就是阻塞状态,浅度睡眠,可以被唤醒

D 磁盘睡眠,也是阻塞状态,称为深度睡眠,页面置换以后也不会解决资源不足的问题,操作系统会杀死该进程。让进程在等待磁盘写入完毕期间,这个进程不能被任何人杀掉。这种状态叫做D状态。写完后再转入运行状态。系统中出现大量D状态,系统很大可能会挂掉。磁盘的压力很大了。D状态不能响应任何请求。dd命令可以实现D状态的实验。

T 停止态,暂停状态发送19号信号。可以转为停止态,发送18号信号恢复。

t 暂停的一种,gdb调试时碰到断点进程的状态转为t

X 死亡态,也就是终止态。释放所有的系统资源

Z 僵尸态,死亡之时会先进入该状态,再进入X状态。父进程关心进程退出时的情况。子进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态,进程的相关资源尤其时task_struct结构体不能被释放。僵尸进程的资源会被一直占用。造成了内存泄露的问题。

退出父进程不进行等待子进程为什么没了呢?

父进程先退出,子进程不退出。子进程的PPID变成了1。那么这个1是systemd这就是操作系统本身。父子进程,父进程先退出,子进程的父进程会被改成1号进程(操作系统),父进程是1号进程的子进程称为孤儿进程,该进程被系统领养。被领养是因为孤儿进程未来也会退出,也要被释放。

进程状态转换图

进程的优先级

是什么?

优先级(对于资源的访问,谁先访问,谁先访问) vs 权限(能不能的问题)

为什么?

因为资源是有限的,进程是多个的,注定了进程之间是竞争关系 --- 竞争性。操作系统必须保证大家良性竞争,确认优先级。如果进程长时间得不到CPU资源,该进程的代码长时间无法得到推进 --- 该进程的饥饿问题。

怎么办?

查看进程的优先级 ps -al

PRI 优先级

NI nice这个是进程优先级的修正数据,Linux不想让nice值过多的参与优先级的调整,需要在我们对应的范围内进行优先级调整,范围为nice:[-20,19] ,80->[60,99]。命令nice和renice直接调整。

我们这里通过top命令更改优先级

PRI(new) = PRI(old) + nice

优先级值越小,优先级越高,优先级值越大,优先级越低

PRI(old)永远是80。

操作系统是如何根据优先级开展调度呢?

每一个CPU都要维护一个运行队列

Linux 2.6 内核中O(1)调度算法,利用位图实现的。

环境变量(《深入理解Linux内核》《深入理解计算机系统》)

首先理解一个概念,并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。它是基于进程切换和时间片轮转的调度算法来实现的。

进程是怎么样进行切换呢?

        进程在从CPU上离开的时候,要将自己的上下文数据保存好,保存好的目的是为了在下一次时间片到达的时候恢复该进程的数据,接着之前的运行结果再次运行。进程在切换的时候要经历两个阶段1、保存上下文。2、恢复上下文。这个过程称为进程切换。

struct reg_info
{
    int eax;
    int ebc;
    int eip;
    ......
}
//实际上我们CPU保存上下文数据是软硬件结合,在这里我们认为
//数据是保存到进程PCB中的。

为什么函数返回值,会被外部拿到呢?

        因为它会被放到 eax 寄存器中比如 mov eax 10。所以我们的返回值是通过CPU寄存器拿到的。

系统如何得知我们的进程当前执行到那行代码了?

        我们的CPU里有一个程序计数器PC或eip,这个计数器会记录当前进程正在指令的下一行指令的地址!

我们的CPU里面有很多的通用寄存器比如:eax,ebx,ecx,edx等,形成栈帧结构的ebp,esp,eip。状态寄存器:status,寄存器在整个CPU起着提高效率,进程高频数据放入寄存器中的作用。CPU内部的寄存器里面保存的是进程相关的数据!CPU寄存器里面保存的是进程的临时数据。把这部分数据称为当前运行进程的上下文数据。

接下里我们了解环境变量的基本概念

1、认识环境变量是干什么的,

        查看环境变量的内容echo $PATH,

用冒号作为分割符

PATH:Linux系统的指令搜索路径,我们也可以为自己的程序写入环境变量,步骤如下

 

这样写会把原有的环境变量覆盖。重新登录Xshell就可恢复了。

所以我们可以这样写

这样我们就可以用指令的方式跑自己的程序了

HOME环境变量

查找所有的环境变量env

在程序中获得环境变量可以使用系统调用接口

USER环境变量。

2、什么是环境变量

环境变量是系统提供的一组name=value形式的变量,不同的环境变量有不同的用户,通常具有全局属性。

穿插一个知识点:命令行参数

C/C++中的main函数是可以传参的。

int main(int argc,char* argv[],char* env[]){}

argc 指针数组有多少个元素

argv 指针数组

命令行参数为指令,工具,软件等提供命令行选项的支持。

观察下面的例子我们就可以明白上面一句话的原理

main函数还有其他的参数,这个参数称为char* env[]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//main函数的三个参数
int main(int argc,char* argv[],char* env[])
{
  if(argc!=2)
  {
    printf("Usage:%s -[a|b|c|d]\n",argv[0]);
    return 0;
  }
  //env数组会以NULL指针结尾
  if(strcmp(argv[1],"-a") == 0)
  {
    printf("功能一:查看环境变量!");
    int i = 0;
    for(;env[i];i++)
    {
      printf("env[%d]:%s",i,env[i]);
      printf("\n");
    }
  }
  else if(strcmp(argv[1],"-b")== 0)
  {
    printf("功能二\n");
  }
  else if(strcmp(argv[1],"-c")== 0)
  {
    printf("功能三\n");
  }
  else if(strcmp(argv[1],"-d")== 0)
  {
    printf("功能四\n");
  }
  else
  {
    printf("default功能\n");
  }
//  char user[32];
//  strcpy(user,getenv("USER"));
//  if(strcmp(user,"root") == 0)
//  {
//    printf("你是一个root用户可以做任何事情\n");
//  }
//  else
//  {
//    printf("你是一个普通用户受权限的限制\n");
//  }
  return 0;
}

会有两张核心向量表:

1、命令行参数表

2、环境变量表:每个程序都会收到一张环境表,环境表就是一个字符指针数组,每个指针指向一个以'\0'结尾的环境字符串。

结论:我们所运行的进程都是子进程,bash本身在启动的时候,会从操作系统的配置文件中读取环境变量信息。子进程会继承父进程交给我的环境变量。所以环境变量有全局属性。

那么怎么证明这件事情呢?

增加我们的环境变量

去查看我们的进程是否有这条环境变量。

验证:

取消环境变量unset

本地变量和内建命令

在命令行中直接定义,set查找系统中的所有变量

本地变量只在bash内部有效,它的子进程不会继承本地变量。提示符就是在bash内部使用的包括

命令可以被分为两批:

1、常规命令 --- 通过创建子进程完成的。

2、内建命令 --- bash不创建子进程,而是由自己亲自执行,类似于bash调用了自己写,或者系统提供的函数。包括echo,cd命令

设置自己的cd命令

也可以这样获取环境变量

进程地址空间

首先来一段代码来验证一下这个概念

运行结果:

下面的图称为地址空间

栈区向下增长,堆区向上增长。

 未初始化全局变量区和已初始化全局变量区称为全局变量区。

解释static:static修饰的局部变量,编译的时候已经被编译到全局变量区了。

下来我们再看一个代码:

#include <stdio.h>
#include <unistd.h>
int gl_val = 10;
int main()
{
  // 创建子进程
  pid_t id = fork();
  int cnt = 5;
  if (id == 0)
  {
    // 子进程
    while (1)
    {
      printf("我是子进程,pid:%d,ppid:%d,gl_val:%d,&gl_val:%p\n", getpid(), getppid(), gl_val, &gl_val);
      
      cnt-=1;
      if (cnt == 0)
      {
        gl_val = 100;
        printf("子进程改变gl_val 10 => 100\n");
      }
      sleep(1);
    }
  }
  else
  {
    // 父进程
    while (1)
    {
      printf("我是父进程,pid:%d,ppid:%d,gl_val:%d,&gl_val:%p\n", getpid(), getppid(), gl_val, &gl_val);
      sleep(1);
    }
  }
  return 0;
}

怎么可能同一个变量,同一个地址同时读取,读到了不同的内容

结论:如果变量的地址,是物理地址,不可能存在上面的现象!所以它绝对不是物理地址。而是线性地址或者虚拟地址。所以我们平时写的C/C++,用的指针,指针里面的地址绝对不是物理地址。

 引入新概念,初步理解这种现象 --- 引入地址空间的概念

 

先经过写时拷贝 --- 是由操作系统自动完成的!

重新开辟空间,但是在这个过程中,左侧的虚拟地址时0感知的,不关心不会影响它。

历史问题

fork返回值,id接收后为什么会有两个,就是上面的原因,虚拟地址。

细节问题

1、地址空间究竟是什么?

        什么叫做地址空间?

        在32位计算机当中,有32位的地址和数据总线 ---每一根总线只通过高低电平也就是0,1,有32根地址总线总共有2^32种0,1序列。那么就可以表示2^32*1byte = 4GB的空间。

地址总线排列组合形成的地址范围就似乎地址空间。

        如何理解地址空间上的区域划分?

        这里可以理解为三八线,这就叫做区域划分,用计算机语言描述就是:


      所谓的空间区域的调整:

不仅仅要看到划分的地址空间范围,在范围内,连续的空间中,每一个最小单位都可以有地址,这个地址可以被直接使用。

所以什么是地址空间呢? 

所谓的地址空间,本质是一个描述进程可视范围的大小。地址空间内一定要存在各种区域的划分,对线性地址进行start,end管理即可。地址空间本质是内核的一个数据结构对象,类似PCB 一样,地址空间也是要被操作系统管理的:先描述再组织。如下。在32位操作系统内的区域为4GB的空间。

2、所以什么叫做进程,以及进程地址空间,为什么要有进程地址空间?

        让所有的进程以统一的视角看待内存。

        增加进程虚拟地址空间可以让我们访问内存的时候,则更加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,所以一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存。

        因为有地址空间和页表的存在,将进程管理模块和内存管理模块进行解耦合。

3、页表

页表当下认为有三个字段虚拟地址,物理地址,标志位读写(标识该物理地址是否为可读可写)

CPU有中cr3寄存器保存页表的地址,这个地址为物理地址。本质属于软件的上下文。

代码是只读的,字符常量区只读的,为什么?

这是因为,页表的标识符字段为只读的。就会拦截进程的读操作。

进程是可以被挂起的,怎么会知道被挂起的在不在内存?

 共识:现代操作系统,几乎不做任何浪费时间和空间的事情。操作系统对大文件可以实现分批加载。惰性加载方式用多少加载多少。那么我们的页表中就必须有一个字段:对应的代码和数据是否已经被加载到内存。若没有加载到内存,操作系统会触发缺页中断把代码和数据从磁盘中加载到内存中,然后建立对应的虚拟地址和物理地址的映射关系。写时拷贝也是这样做到的。

进程在被创建的时候,是先创建内核数据结构,在加载对应的可执行程序和数据。Linux的内存管理模块实现的。

所以我们的进程概念就变成了

进程 = 内核数据结构(task_struct&&mm_struct&&页表) + 程序的代码和数据。

进程的在切换的时候这些数据结构就随着PCB的切换就都切换了。

进程具有独立性,怎么做到的?

因为页表的存在。

 命令行参数和环境变量是在栈区之上的。

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值