进程控制,父子进程

发布于:2022-11-08 ⋅ 阅读:(642) ⋅ 点赞:(0)

学习重点

  1. 进程控制

    主要需要通过进程控制回答以下问题:

    • 在OS的支持下,进程具体是如何启动起来的?

      详细介绍程序是如何运行起来,演变为进程的

      • 也就是通过exec加载程序后,进程启动起来的具体过程

      学完本部分就会知道:

      双击快捷图标,或者在命令行执行./a.out后,程序运行起来的背后原理

    • 有OS支持时,main函数return,或者使用exit、_exit所返回的返回值到底给了谁,有什么意义

    为什么在平时,这个返回值我们会感觉到并没有什么用处

    • java的进程是如何运行起来的

    程序在内存中运行起来后就演变为了进程

    • 但是由于java需要虚拟机来解释执行,中间夹一个虚拟机,所以要单独理解java进程是如何产生的

    与java类似的C#、pyhton等的程序,都是相同的运行过程

    • 什么情况下的程序才会涉及到多进程呢

      不过一般我们写程序都是单进程

  2. 进程关系

​ 基于OS运行的进程很多,众多进程之间往往存在着一定的关系,在这一部分会具体介绍进程之间有哪些关系

  1. 守护进程

    OS启动起来以后,会有很多我们平时看不见的,但是一直在默默运行的后台进程,这些后台进程大多其实是守护进程

进程控制

  • 对于程序员来说,编写程序时有各种语言的区分
  • 但是文字编码形式的程序一旦被转为机器指令被CPU执行时,对于CPU来说全都是一样的,没有任何区别
  • 所有程序(c++,java)在Linux OS上运行起来,演变为进程时,都有着相同的启动过程
  • 有关进程控制的API,可以用来加载运行所有语言编写的程序

进程相关知识

将程序代码从硬盘拷贝到内存上,在内存上动态运行的程序就是进程

存储位置 存在状态 运行过程
程序 硬盘 静态
进程 内存上,从硬盘拷贝的副本 动态 进程有生有死

多进程并发运行

有OS支持时,会有很多的进程在运行,这些进程都是并发运行

并发运行:

就是CPU轮换的执行

当前进程执行了一个短暂的时间片(ms)后,切换执行另一个进程,如此循环往复

  • 由于时间片很短,在宏观上我们会感觉到所有的进程都是在同时运行的
  • 但是在微观上cpu每次只执行某一个进程的指令

当然我们这里说的单核cpu的情况

多核CPU:并发与并行是同时存在的

进程号

OS为了能够更好地管理进程,为每个进程分配了一个唯一的编号(非负整数),这个编号就是PID

PID用来唯一标识一个进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S3FVtkIc-1667904736804)(/home/guojiawei/.config/Typora/typora-user-images/image-20221108152039707.png)]

​ 如果当前进程结束了,这个PID可以被可以被重复使用,但是所有“活着”的进程,它们的进程ID一定都是唯一的

  • 进程号在系统调用时可以作为传入参数或者返回值
PID放在了哪里?

PID放在了该进程的task_struct结构体变量中

进程在运行的过程中:OS会去管理进程,这就涉及到很多的管理信息;

OS为了管理进程,会为每一个进程创建一个task_struct结构体变量,里面放了各种的该进程的管理信息

三个特殊进程0,1,2

0、1、2这个三个进程,是OS启动起来后会一直默默运行的进程,直到关机OS结束运行

这三个进程非常重要

  1. 进程 PID == 0

    • 这个进程被称为调度进程

    • 功能是实现进程间的调度和切换

      该进程根据调度算法,让CPU轮换的执行所有的进程

    • 当pc指向不同的进程时,cpu就去执行不同的进程,这样就能实现切换,同时保存被切换进程的现场

    这个进程怎么来的?
    这个进程就是由OS演变来的,OS启动起来后,最后有一部分代码会持续的运行,这个就是PID==0的进程。
    由于这个进程是OS的一部分,凡是由OS代码演变来的进程,都称之为系统进程。
    
  2. 进程 PID == 1

    • 作用:

      1. 初始化
      2. 托管孤儿进程
      3. 原始父进程-----就是它是最父的进程,用来创建子进程

    初始化

    • 这个进程被称为init进程

    • 作用是:

      它会去读取各种各样的系统文件,使用文件中的数据来初始化OS的启动,让我们的OS进入多用户状态,也就是让OS支持多用户的登录

    • 这个进程不是OS演变来的,也就是说这个进程的代码不属于OS的代码,这个进程是一个独立的程序
    • 该PID==1的程序代码放在了/sbin/init下,当OS启动起来后,OS会去执行init程序,将它的代码加载到内存,这个进程就运行起来了
  3. 进程 PID == 2

  • 该进程是守护进程或精灵进程,精灵进程也叫守护进程
  • 作用:专门负责虚拟内存的请页操作

当OS支持虚拟内存机制时,加载应用程序到内存时,并不会进行完整的代码拷贝,只会拷贝当前要运行的那部分代码,当这部分代码运行完毕后,会再拷贝另一部分需要运行的代码到内存中,拷贝时是按照一页一页来操作的,每一页大概4096字节,这就是换页操作

与调度进程一样,也是一个系统进程,代码属于OS的一部分

获取进程相关的ID函数

#include <sys/types.h>
#include <unistd.h>
//获取调用该函数进程的进程ID(哪个函数调用它,返回的就是哪个)
pid_t getpid(void);
//p:parent p:process  
//获取调用该函数进程的父进程ID
pid_t getppid(void);
#include <unistd.h>
#include <sys/types.h>
//获取调用该函数进程的用户ID(在哪个用户下调用该进程)
uid_t getuid(void);
//获取用户组的ID,也就是调用该函数的那个进程,它的用户所在用户组的组ID
uid_t getgid(void);

返回值:返回各种ID值,不会调用失败,永远都是成功的

#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main(void){
    //pid_t getpid(void);
    pid_t pid=getpid();
    printf("%u\n",pid);
    //pid_t getppid(void);
    pid_t pid_parent=getppid();
    printf("%u\n",pid_parent);
    //uid_t getuid(void);
    uid_t uid=getuid();
    printf("%d\n",uid);
    //uid_t geteuid(void);
    uid_t gid=getgid();
    printf("%d\n",gid);
    return 0;
}

父进程就是当前终端窗口bash,因为在这个窗口执行的。

vi /etc/passwd

guojiawei❌1000:1000:guojiawei,:/home/guojiawei:/bin/bash

fork()

程序运行的三个过程:

(1)在内存中划出一片内存空间
(2)将硬盘上可执行文件中的代码(机器指令)拷贝到划出的内存空间中
(3)pc指向第一条指令,cpu取指运行

当有OS时,以上过程肯定都是通过调用相应的API来实现的。

在Linux下,OS提供两个非常关键的API,一个是fork,另一个是exec

  • fork:开辟出一块内存空间

  • exec:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,CPU开始执行,进程就运行起来了

    运行起来的进程会与其它的进程切换着并发运行

#include <sys/types.h>
#include <unistd.h>
//pid_t  无符号整型
//从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程
pid_t fork(void);

复制后有两个结果:

  1. 依照父进程内存空间样子,原样复制地开辟出子进程的内存空间
  2. 由于子进程的空间是原样复制的父进程空间,因此子进程内存空间中的代码和数据和父进程完全相同

复制父进程的主要目的:

  • 就是为了复制出一块内存空间

  • 只不过复制的附带效果是,子进程原样的拷贝了一份父进程的代码和数据

    事实上复制出子进程内存空间的主要目的,其实是为了exec加载新程序的代码

   由于子进程原样复制了父进程的代码,因此父子进程都会执行fork函数
        "当然这个说法有些欠妥,但是暂且这么理解"
	1)父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。
	2)子进程的fork,成功返回0,失败返回-1,errno被设置。
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main(void){
    //pid_t fork(void);
    pid_t ret=fork();
    printf("%d,ret=%d\n",getpid(),ret);
    while(1);//为了能够查到进程pid,不然程序结束后会被杀死
    return 0;
}
/*
guojiawei@ubantu-gjw:~/Desktop/process_control$ ./a.out 
12648,ret=12649
12649,ret=0
*/

#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main(void){
    pid_t ret=fork();
    /*区分父子进程*/
    if(ret >0){//父进程返回值是子进程pid,大于0
        printf("parent PID=%d\n",getpid());
        printf("parent ret=%d\n",ret);
    }
    else if(ret ==0){//子进程返回0
        printf("child PID=%d\n",getpid());
        printf("child ret=%d\n",ret);
    }
    printf("hello,world\n");
    return 0;
}
guojiawei@ubantu-gjw:~/Desktop/process_control$ ./a.out 
parent PID=13154
parent ret=13155
hello,world
child PID=13155
child ret=0
hello,world

上述代码:父子进程各有一份拷贝,但是父子进程可以根据if条件各自做不同的事

复制的原理
  • Linux有虚拟内存机制,所以父进程是运行在虚拟内存上的,虚拟内存是OS通过数据结构基于物理内存模拟出来的,因此底层的对应的还是物理内存。
  • 复制时子进程时,会复制父进程的虚拟内存数据结构,那么就得到了子进程的虚拟内存,相应的底层会对应着一片新的物理内存空间,里面放了与父进程一模一样代码和数据

父进程在os上基于物理内存复制出一系列数据结构得到虚拟内存;子进程fork时候,完全复制父进程的代码和数据,而且在新开辟的内存中存储和父进程一样的内容

此时只要通过exec在子进程中加载新的程序,就可以实现开辟内存的效果

父子进程各自执行哪些代码
int main(void){
    printf("before fork()\n");
    pid_t ret=fork();
    /*区分父子进程*/
    if(ret >0){//父进程返回值是子进程pid,大于0
        printf("parent PID=%d\n",getpid());
        printf("parent ret=%d\n",ret);
    }
    else if(ret ==0){//子进程返回0
        printf("child PID=%d\n",getpid());
        printf("child ret=%d\n",ret);
    }
    printf("after fork\n");
    return 0;
}
guojiawei@ubantu-gjw:~/Desktop/process_control$ ./a.out 
before fork()
parent PID=14206
parent ret=14207
after fork
child PID=14207
child ret=0
after fork

父子进程代码都相同

  • 父进程会执行fork()之前的语句,子进程只会执行fork()之后的语句
  • 父子进程的返回值不同,会根据if条件选择性执行

尽管子进程复制了这段代码,但是子进程并不会执行,子进程只从fork开始执行

解释子进程打印出:before fork()

  • 首先子进程复制了整个父进程程序,但是子进程不会执行fork()之前的语句
  • printf()是行缓冲,如果没\n,也就是没写满一行,会挤压在缓冲区
  • 这里的before fork()是直接从父进程复制的数据结构,然后随着第一个\n一起打印

父子进程共享操作文件

分为两种情况:

  • 情况1:父子进程分别独立打开同一个文件

  • 情况2:fork之前打开文件,也就是父进程事先打开,父子进程共享

独立打开文件

涉及到的两个进程是父子关系,独立打开同一文件实现共享操作

#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(void){
   pid_t ret=0;
   int fd=0;
   ret=fork();//创建子进程
   if(ret>0){//父进程操作
      fd=open("mm.txt",O_RDWR|O_CREAT,0664);
      write(fd,"hello\n",6);
   }
   else if(ret==0){//子进程操作
      fd=open("mm.txt",O_RDWR|O_CREAT,0664);
      write(fd,"world\n",6);
   }
   return 0;
}
/*
执行结果:world-------子进程覆盖了父进程
*/

原因:

由于父子进程单独打开文件,所以父子进程各自的进程表中,存放着单独的文件表,笔尖位置相同,由于打开同一个文件,最终节点指针指向同一个V节点,从而发生了覆盖

  • 独立打开同一文件时,父子进程各自的文件描述符,指向的是不同的文件表

  • 如果不想相互覆盖,需要加O_APPEND标志。

  • #include <sys/types.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdio.h>
    int main(void){
       pid_t ret=0;
       int fd=0;
       ret=fork();//创建子进程
       if(ret>0){//父进程操作
          fd=open("mm.txt",O_RDWR|O_CREAT|O_APPEND,0664);
          write(fd,"hello\n",6);
       }
       else if(ret==0){//子进程操作
          fd=open("mm.txt",O_RDWR|O_CREAT|O_APPEND,0664);
          write(fd,"world\n",6);
       }
       return 0;
    }
    

fork之前打开文件

#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(void){
   pid_t ret=0;
   int fd=open("mm.txt",O_RDWR|O_CREAT|O_TRUNC,0664);
   ret=fork();//创建子进程
   if(ret>0){//父进程操作
      write(fd,"hello\n",6);
   }
   else if(ret==0){//子进程操作
      write(fd,"world\n",6);
   }
   return 0;
}

子进程会继承父进程已经打开的文件描述符,如果父进程的3描述符指向了某个文件,子进程所继承的文件描述符3也会指向这个文件

像这种继承的情况,父子进程这两个相同的“文件描述符”指向的是相同的“文件表”

0,1,2文件:

​ 子进程的0 1 2这三个打开的文件描述符,其实也是从父进程那里继承过来的,并不是子进程自己去打开的,同样的父进程的0 1 2又是从它的父进程那里继承过来的,最根溯源的话,都是从最原始的进程哪里继承过来的

init进程会去打开标准输入,标注输出、标准出错输出这三个文件,然后0 1 2分别指向打开的文件,之后所有进程的0 1 2,实际上都是从最开始的init进程那里继承而来的。

子进程会继承父进程的属性

     	(1)用户ID,用户组ID
		(2)进程组ID
		(3)会话期ID
		(4)控制终端
		(5)当前工作目录
		(6)根目录(/)
		(7)文件创建方式屏蔽字(unmask)
		(8)环境变量
		(9)打开的文件描述符
			等等

子进程独立的属性:
(1)进程ID
(2)不同的父进程ID
(3)父进程设置的锁,子进程不能被继承


网站公告

今日签到

点亮在社区的每一天
去签到