12.OS操作系统学习_进程空间与控制

12.OS操作系统学习:进程空间与控制

1.内存中的进程

我们思考

多进程运行,如何更好的区分空间?

写时拷贝机制原理是什么呢?

我们先来看一下这个学习到的图像:内存中的进程image

利用fork()函数创建子进程,使子进程和父进程共同使用一个变量。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
  int val = 10;
  pid_t id = fork();
  if(id == 0)
  {
  	val *= 2;	//刻意改变共享值
    printf("我是子进程,pid:%d ppid:%d 共享值:%d 共享值地址:%p\n", getpid(), getppid(), val, &val);
    exit(0);
  }

  waitpid(id, 0, 0);

  printf("我是父进程,pid:%d ppid:%d 共享值:%d 共享值地址:%p\n", getpid(), getppid(), val, &val);
  return 0;
}

image

现象:会发现同一个地址,出现了两个不同的value。

**解释:1.**引出逻辑地址:面向程序的地址,从0开始编址,每一条指令的逻辑地址就是与第一条指令之间的相对偏移。对于面向程序的地址,用户无法看到真实的绝对地址(物理地址),他们是由os统一管理的。

2.真实的物理空间不是相同的

3.发生了写时拷贝机制

2.虚拟地址和物理地址的转换

虚拟空间通过notebook和MMU进行转换为真实的空间

2.1先来看MMU:

image

2.2再来看页表

页表 本质上就是一张表,操作系统 会为每个 进程 分配一个 页表,该 页表 使用 物理地址 存储。当 进程 使用类似 malloc 等需要 映射代码或数据 的操作时,操作系统 会在随后马上 修改页表 以加入新的 物理内存。当 进程 完成退出时,内核会将相关的页表项删除掉,以便分配给新的 进程

2.3转换

image

image

解释:

  1. 其实操作系统通过页表映射发现val的值是共享的,但是进程具有独立性

  2. 操作系统 为 保证进程独立性,当任何一方对共享数据进行写=修改的时候,操作系统会在真的物理空间上开辟一块新的内存空间.

  3. 先拷贝数据,然后再修改映射关系,更改页表映射,然后再让进程进行修改.

  4. 所以这里的显示的地址是虚拟地址,也是相同的.

2.4 写时拷贝

定义:在数据第一次写入到某个存储位置时,首先将原有内容拷贝出来,写到另一位置处,然后再将数据写入到存储设备中,该技术只拷贝在拷贝初始化开始之后修改过的数据。

这里的写时拷贝是操作系统先进性数据拷贝,更改页表映射,然后再让进程进行修改.

3.为什么存在进程空间?

1.进程地址空间保证了数据的安全性

:::
每个进程都有进程地址空间,所有的进程都要通过页表映射到物理内存,如果进程直接访问物理内存,万一进程越界非法访问、非法读写时,页表就可以进行拦截,而且直接访问物理内存对于账号信息是非常不安全的,所以保证了内存数据的安全性
:::

2.地址空间的存在,可以更方便的进行进程和进程的数据代码的解耦,保证了进程独立性的特征

:::
对于进程而言,都有独立的地址空间及页表,通过页表映射到不同的物理内存上,所以一个进程数据的改变不会影响到另一个进程,保证了进程的独立性,而对于上面我们所说的父进程和子进程而言,子进程的地址空间从父进程拷贝,页表都指向同一块物理内存,但是即使此时的数据是共享的,在修改数据的时候也会发生我们所说的写时拷贝,保证了进程的独立性
:::

3.编译器也以统一的视角来进行编译代码

4.进程的控制

1.创建

1.1创建fork()

#include <unistd.h>	//所需头文件
pid_t fork(void);	//fork 函数

image

理解:

**1.**fork()函数再操作系统内部,子进程被创建的时候,进程都在ready队列中,准备被调度,fork()之后父子进程代码被共享,return会被调度两次.

**2.**父进程返回子进程的PID,给子进程返回0

**3.**pid_t 相当于typedef int

**4.**当进程过多或者用户进程数超过限制的时候,fork()会失败

1.2写时拷贝机制

image

image

2.终止

2.1查看最近一次进程运行的退出码

echo $?

进程退出后,os会释放对应的内核数据结构、代码和数据

main函数退出,表示整个程序退出

程序中的函数退出,函数运行结束

2.2退出方式

1.对于运行的进程:kill -9 PID和ctrl+c的方式
2.内部终止通过函数exit()和_exit()实现
void exit(int status);
void _exit(int status);

但是使用上其实是有区别的:

image

由此可见:exit()是要刷新缓冲区的

practice:

test31.cpp

image

现象

image

test32.cpp

image

现象

image

3.exit和return的区别

1.exit是函数;return是关键字

2.exit是系统调用级别的,它表示一个进程的结束;return是语言级别的,它表示调用堆栈的返回

3.exit是进程的退出;return是函数的退出

4.exit函数结束进程,删除进程使用的内存空间,并将进程的状态返回给操作系统(一般是用0表示正常终止,非0表示异常终止);return是结束函数的执行,将函数的执行信息传其他调用函数使用

5…非主函数中调用exit和return区别很明显,但是在main函数中调用区分不大,多数情况效果一样

3.等待

理论

image

之前在11章时就讲过当父进程没有等待并接收其退出码和退出状态的时候,OS无法释放对应的内核数据结构+代码和数据,出现僵尸进程

  1. 为了避免这种情况,父进程可以通过函数等待子进程运行结束,此时父进程还处于阻塞状态

  2. 子进程必须对子进程进行负责

等待函数

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);

pid_t waitpid(pid_t pid, int* status, int options);
wait:
  1. wait成功返回被等待进程pid,等待时返回0,失败返回-1

  2. 参数:不关心子进程退出可以设置为NULL

waitpid:

关于三个参数:

1.**pid** 表示所等子进程的 **PID**

2.**status** 该参数是一个输出型参数,由操作系统填充 ,如果传递NULL,表示不关心子进程的退出状态信息。否则**,操作系统会根据该参数,将子进程的退出信息反馈给父进程**。status不能简单的当作整形来看待,可以当作位图来看待

image

3.**options** 为选项,比如可以选择父进程是否需要阻塞等待子进程退出,默认为0

演示:
#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>

int main()
{
  //演示 waitpid()
  pid_t id = fork();  //创建子进程
  if(id == 0)
  {
    int time = 5;
    int n = 0;
    while(n < time)
      {
        printf("我是子进程,我已经运行了:%d秒 PID:%d   PPID:%d\n", n + 1, getpid(), getppid());
        sleep(1);
        n++;
      }

    exit(244);  //子进程退出
  }

  int status = 0; //状态
  pid_t ret = waitpid(id, &status, 0); //参数3 为0,为默认选项

  if(ret == -1)
  {
    printf("进程等待失败!进程不存在!\n");
  }
  else if(ret == 0)
  {
    printf("子进程还在运行中!\n");
  }
  else
  {
    printf("进程等待成功,子进程已被回收\n");
  }

  printf("我是父进程, PID:%d   PPID:%d\n", getpid(), getppid());

  //通过 status 判断子进程运行情况
  if((status & 0x7F))
  {
    printf("子进程异常退出,core dump:%d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
  }
  else
  {
    printf("子进程正常退出,退出码:%d\n", (status >> 8) & 0xFF);
  }

  return 0;
}

代码解释:

  if((status & 0x7F))
  {
    printf("子进程异常退出,core dump:%d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
  }
  else
  {
    printf("子进程正常退出,退出码:%d\n", (status >> 8) & 0xFF);
  }
  1. 异常退出检查:status & 0x7F:这里使用按位与操作检查 status 的低 7 位。如果结果不为零,表示子进程是由于接收到信号而退出的,而不是正常退出的。

image

可以使用(**WIFEXITED(status)** **判断进程退出情况,当宏为真时,表示进程正常退出)**代替

  1. (status >> 8) & 0xFF:这一部分提取了 status 的高 8 位,表示子进程的退出码

image

可以使用(**WEXITSTATUS(status)** 相当于 **(status >> 8) & 0xFF****,直接获取退出码)**代替

现象:

image

等待时执行

options参数改为WNOHANG,也就是父进程处于非阻塞状态,父进程进入等待轮询状态,不断获取子进程是否退出,如果没有退出,就可以做别的事情。

waitpid(id, &status, WNOHANG);
pracitice:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> //进程等待相关函数头文件
#include<stdlib.h>
int main()
{
  //演示 waitpid()
  pid_t id = fork();  //创建子进程
  if(id == 0)
  {
    int time = 9;
    int n = 0;
    while(n < time)
    {
      printf("我是子进程,我已经运行了:%d秒 PID:%d   PPID:%d\n", n + 1, getpid(), getppid());
      sleep(1);
      n++;
    }

    exit(244);  //子进程退出
  }

  int status = 0; //状态
  pid_t ret = 0;
  while(1)
  {

    ret = waitpid(id, &status, WNOHANG); //参数3 设置为非阻塞状态
    
    if(ret == -1)
    {
      printf("进程等待失败!进程不存在!\n");
      break;
    }
    else if(ret == 0)
    { 
      printf("子进程还在运行中!\n");
      printf("我可以干一些其他任务\n");
      sleep(3);
    }
    else
    {
      printf("进程等待成功,子进程已被回收\n");
      //通过 status 判断子进程运行情况
      if(WIFEXITED(status))
      {
        printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
        break;
      }
      else
      {
        printf("子进程异常退出,code dump:%d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
        break;
      }
    }
  }

  return 0;
}

现象:

image

image

4.进程替换

4.1  为什么要进行进程替换?

image

4.2 七大替换函数

1.excel  使用list方式
#include <unistd.h>

int execl(const char* path, const char* arg, ...);

#include <stdio.h>
#include <unistd.h>

int main()
{
  //execl 函数
  printf("程序替换前,you can see me\n");
  int ret = execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

  //程序替换多发生于子进程,也可以通过子进程的退出码来判断是否替换成功
  if(ret == -1)
    printf("程序替换失败!\n");

  printf("程序替换后,you can see me again?\n");
  return 0;
}

链式传递方式:

image

现象:

image

2.execv  使用vector方式
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
  //execv 函数
  pid_t id = fork();
  if(id == 0)
  {
    printf("子进程创建成功 PID:%d   PPID:%d\n", getpid(), getppid());
    char* const argv[] = 
    {
      "ls",
      "-a",
      "-l",
      NULL
    };	//argv 表,实际为指针数组

    execv("/usr/bin/ls", argv);

    printf("程序替换失败\n");
    exit(255); //如果子进程有此退出码,说明替换失败
  }

  int status = 0;
  waitpid(id, &status, 0); //父进程阻塞等待
  if(WEXITSTATUS(status) != 255)
  {
    printf("子进程替换成功,程序正常运行 exit_code:%d\n", WEXITSTATUS(status));
  }
  else
  {
    printf("子进程替换失败,异常终止 exit_code:%d\n", WEXITSTATUS(status));
  }

  return 0;
}

现象:

image

解释:WEXITSTATUS(status) != 255表示退出码如果不等于255就代表程序替换成功

3.execlp 可以自动到PATH变量中搜索
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
  //execlp 函数
  pid_t id = fork();
  if(id == 0)
  {
    printf("you can see me\n");

    execlp("ls", "ls", "-a", "-l", NULL); //程序替换

    printf("you can see me again?");
    exit(-1);
  }

  int status = 0;
  waitpid(id, &status, 0);  //等待阻塞
  if(WEXITSTATUS(status) != 255)
    printf("子进程替换成功 exit_code:%d\n", WEXITSTATUS(status));
  else
    printf("子进程替换失败 exit_code:%d\n", WEXITSTATUS(status));

  return 0;
}

现象:

image

4.execvp  可以自动到PATH变量中搜索
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
  //execvp 函数
  pid_t id = fork();
  if(id == 0)
  {
    printf("子进程创建成功 PID:%d   PPID:%d\n", getpid(), getppid());
    char* const argv[] = 
    {
      "ls",
      "-a",
      "-l",
      NULL
    };

    execvp("ls", argv);

    printf("程序替换失败\n");
    exit(-1); //如果子进程有此退出码,说明替换失败
  }

  int status = 0;
  waitpid(id, &status, 0); //父进程阻塞等待
  if(WEXITSTATUS(status) != 255)
  {
    printf("子进程替换成功,程序正常运行 exit_code:%d\n", WEXITSTATUS(status));
  }
  else
  {
    printf("子进程替换失败,异常终止 exit_code:%d\n", WEXITSTATUS(status));
  }

  return 0;
}
5.execle 将自定义或当前程序中的环境变量表传给代替换程序

程序1:execle1.cpp

#include<stdio.h>
#include<stdlib.h>
#include<iostream>
using namespace std;
extern char** environ;	//声明环境变量表nmain(int argc,char *argv[])
int main(int argc,char* argv[])
{
  int pos=0;
  while(environ[pos])
  {
    cout << environ[pos++] << endl;
  }
  for(int i=0;argv[i];i++)
  {
    printf("argv[%d]:%s\n",i,argv[i]);
  }
  printf("PATH:%s\n",getenv("PATH"));
  printf("PWD:%s\n",getenv("PWD"));
  printf("传递测试程序\n");
  return 0;
}

程序2:execle2.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
extern char** environ;
int main()
{
  printf("process is running...\n");
  pid_t id =fork();
  assert(id!=-1);
  if(id==0)
  {
    sleep(1);
    putenv("myval=100");
    
    execle("./execle1","execle1","-a","-b",NULL,environ);
  }
  pid_t rid = waitpid(id,NULL,0);
  if(rid>0)
  {
    printf("wait sucess\n");
  }

  return 0;
}

实验现象:

image

image

红框:printf

黄框:存储程序的命令行参数

灰框:环境变量

6.execvpe和execvp但是最后一个参数可以传递环境变量表
#include <unistd.h>

int execvpe(const char* file, char* const argv[], char* const envp[]);
7.execve 真正的程序替换函数
#include <unistd.h>

int execve(const char* filename, char* const argv[], char* const envp[]);

参数1:待替换程序的路径

参数2:待替换程序名及其参数组成的 **argv** 

参数3:传递给待替换程序的环境变量表

practice:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main(int argc,char* argv[],char* envp[])
{
  pid_t id =fork();
  if(id==0)
  {
    printf("You can see me\n");
    execve("/usr/bin/ls",argv,envp);
    printf("You can see me again?");
    exit(-1);
  }
  int status=0;
  waitpid(id,&status,0);
  if(WEXITSTATUS(status)!=255)
        printf("子进程替换成功 exit_code:%d\n", WEXITSTATUS(status));
  else
    printf("子进程替换失败 exit_code:%d\n", WEXITSTATUS(status));

  return 0;
}

image

4.3 程序替换以后是否还为同一个进程

实验:

  1. practice

    #include
    #include <unistd.h>

    using namespace std;

    int main()
    {
    int n=4;
    while(n)
    {
    cout << “程序替换成功”;
    cout << " PID:" << getpid() << " PPID:" << getppid() << endl;
    sleep(1);
    n–;
    }

    return 0;
    }

2.practice1

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main(int argc,char* argv[],char* envp[])
{
  pid_t id=fork();
  if(id==0)
  {
    printf("111");
    printf("process sucess PID:%d,PPID:%d",getpid(),getppid());
    fflush(stdout);
    execve("./practice",argv,envp);
    exit(-1);
  }
  int status=0;
  waitpid(id,&status,0);
  if(WEXITSTATUS(status)!=255)
        printf("子进程替换成功 exit_code:%d\n", WEXITSTATUS(status));
  else
    printf("子进程替换失败 exit_code:%d\n", WEXITSTATUS(status));

  return 0;
}

image

说明:

1.程序替换不是进程替换

2.因为是同一个进程,所以对父进程没有任何影响,体现了进程间的独立性。

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值