初识Linux下进程2

简介: 初识Linux下进程2

🌎初识Linux下进程(下)




前言

  上回我们简单介绍了一下进程的概念以及让大家见到了运行中的进程,今天我们来了解更多进程相关知识,话不多说,开启我们今天的话题!


🚀系统调用获得父子进程id

  上次我们说,pid是每个进程特有的一个编号,每个进程都有自己的pid,这也是进程的一个属性信息,属于 操作系统内核数据结构, 我们知道,内核数据结构是不能被用户直接拿来使用的,而是需要通过 系统调用 的方式来获取属性信息:

  而 获取进程pid 的系统调用接口就是 getpid,为了了解这个接口,我们可以使用:

man getpid#man手册查看系统调用接口

  我们可以看到,一个程序想要调用系统调用接口getpid,就必须包含 <sys/types.h><unistd.h> 头文件。

  具体的用法是:

  我们运行程序就可以得到这个进程的pid了:

  我们发现,我们运行三次,每一次进程的 pid 都是不一样的,其实这是因为:

  <sys/types.h>进程的pid是系统中一个重要且有限的资源,当程序终止运行时,pid就会被bash回收,而再次运行时,bash会随机分配给你空闲的pid使用。

  我们可以使用一个监控脚本,用来持续控制输出的进程信息:

while :; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep; sleep 1; done#控制进程每隔一秒输出进程信息

  当进程被终止的时候,进程信息也会被bash回收,在进程终止之前,我们看到了进程除了有pid,还有它的ppid,也就是父进程id,那么我们是否可以通过系统调用的方式来获得父进程的pid呢?当然是可以的:

  程序保存退出,将原来的可执行程序make clean 一下,再make一下得到新的可执行程序,然后运行该程序:

  我们可以看到,该进程的id为6331,父进程id为5506,我们知道,如果我们进程重新启动,那么我们进程的pid就有很大概率会被改变,那么我们父进程是否也是如此呢?

  我们可以看到,每一次重新启动进程,进程的id都会改变,但是每次启动进程的时候,进程的ppid确是不会改变的,其实我们查看该父进程:

  我们看到,该进程的父进程就是bash,所以 每次启动时进程的父进程不会改变


🚀进程信息中的路径信息

  我们查看一个普通进程的进程信息时,会发现 exe 和 cwd 两项信息,我们可以使用如下命令进行查看:

ls /proc/进程标识符/ -l#进程标识符为进程的id

  这里我们需要关注到两个重要信息,cwdexe, exe就不用多说了,就是 可执行程序cwd全称叫做(current working directory),表示 当前工作目录

  其实我们还学过于此相关的一个芝士,我们在C语言文件操作那里,有这样一个函数:

fopen("file.txt", "w");

  我们都知道,如果在当前路径下没有这个文件,就会在当前路径下创建这个文件,其实就是在cwd路径下创建,当系统在执行C语言代码时,执行到当前这行就会拿到进程的cwd

  注意进程只有 运行起来的程序 才可以查看进程,所以要查看进程,进程必须要保证在查看的那一刻进程是在运行的。

  但是如果进程是在 运行时被干掉 的,此时我们依然可以查看该进程的信息状态:

  这个时候我们就可以发现,我们的exe状态栏会闪红,并且在最后会提示该进程已经删除。

  但是这里有个疑问:为什么我们把进程删除了还能继续运行

  实际上,我们运行的本质就是 把磁盘中的数据拷贝一份到内存中 来,我们把磁盘中的数据删除了,但是我们内存中的数据还在,所以还是可以运行的。

  我们上面说,fopen函数在执行到改行的时候会 拿到进程的 cwd 然后创建文件,我们不妨做个测试:

1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     printf("myself id: %d\n", getpid());
  8     FILE *fp = fopen("./test.txt", "w");//当前目录下创建一个文件,
  9     if(fp == NULL) return -1;
 10 
 11     fclose(fp);
 12 
 13     sleep(5);//休眠5秒看到结果                         
 14 
 15     return 0;
 16 }

  通过实验我们可以看到,确实是在当前路径下创建的文件,但是我们如果并不想在当前路径下创建文件,想要按照自己指定的路径下来创建文件,我们可以使用 chdir 接口:

  这里的 const char* 表示字符串信息,这个字符串信息是 指定的工作目录,我们不妨做个实验:

1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<unistd.h >
  4 
  5 int main()
  6 {
  7     printf("myself id: %d\n", getpid());
  8     printf("更改当前工作目录之前\n");
  9     sleep(5);
 10 
 11     chdir("home/xzy");//将工作目录更改为家目录
 12     printf("更改工作目录之后\n");
 13     sleep(5);
 14 
 15     FILE *fp = fopen("./test.txt", "w");
 16 
 17     if(fp == NULL) return -1;
 18     fclose(fp);
 19     sleep(5);
 20 
 21     return 0;
 22 }

  我们使用 chdir 来更改工作目录,以至于达成想要的效果。

  更改工作目录之前:

  更改工作目录之后:

  由此,也能证明我们经常使用的fopen函数是从进程的cwd获取路径的。


🚀创建进程

  创建一个进程需要使用到 fork 函数接口:

  fork函数是用来创建子进程的接口,至于到底该如何使用该接口,我们看下面这个例子:

#include<stdio.h>
  2 #include<sys/types.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     printf("before fork, pid=%d, ppid=%d\n",getpid(),getppid());
  8 
  9 
 10     fork();
 11 
 12     printf("after fork, pid=%d, ppid=%d\n",getpid(),getppid());                                                    
 13     return 0;
 14 }

  我们在Shell上运行起来可以观察到:

  运行起来之后,我们发现,fork之前的打印只执行了一次,而fork之后的打印却执行了两次,多次运行都是这个结果,说明并不是偶然现象。

  其实,fork()之后是创建了子进程,执行下面的代码,这样就有两个进程,并且都会执行fork之后的代码。但是fork之前的代码只有父进程执行。

  证明:fork之后,进程ppid(父进程id)就是fork之前进程的pid。


✈️fork的返回值

  fork可以创建子进程,而要控制子进程,就与fork的返回值有关了。

  fork的返回值是pid_t的一种特殊类型,返回值为0时返回到子进程返回值为子进程pid时,返回到父进程,如果返回值小于0表示错误。也就是说,fork其实有两个返回值

 &emps;我们不妨做个实验验证一下:

#include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 
  5 int main()
  6 {
  7     pid_t id = fork();
  8     if(id == 0)
  9     {
 10         printf("this is child process, pid=%d, ppid=%d, forkid = %d\n",getpid(),getppid(),id);
 11     }
 12     printf("this is parent process, pid=%d, ppid=%d, forkid = %d\n",getpid(),getppid(),id);                        
 13     return 0;
 14 }

  根据实验结果来看,fork之后的返回值确实有两个,返回给父进程子进程的pid,返回0给子进程。至于为什么有两个返回值,这里我们说不清,现在只需要记住即可,后面我们会详谈。


✈️子进程的用处

  fork创建子进程,创建子进程的目的一定是为了能够给我们做更多的事情,做父进程以外的事情.

1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 
  5 int main()
  6 {
  7     printf("before fork, porcess pid=%d, ppid=%d\n",getpid(), getppid());//进程创建之前的pid和ppid
  8     sleep(3);
  9     printf("start fork!\n");
 10     sleep(1);
 11 
 12     pid_t id = fork();//开始fork
 13 
 14     if(id < 0)
 15     {
 16         perror("fork fail!\n");
 17         return -1;
 18     }
 19 
 20     if(id == 0) {
 21         //子进程
 22         while(1)
 23         {
 24             printf("after fork, process pid=%d, ppid=%d\n", getpid(), getppid());
 25             sleep(1);
 26         }
 27     }
 28     else {
 29         //父进程
 30         while(1)                                                                                                                                                                                                                          
 31         {
 32             printf("after fork, process pid=%d, ppid=%d\n", getpid(), getppid());
 33             sleep(1);
 34         }
 35     }
 36 
 37     sleep(1);
 38     return 0;
 39 }

  这里我们在父子进程当中都有一个死循环,我们运行程序:

  不难观察到子进程和父进程是在同时运行的,这也验证了,fork之后创建了一个新的进程——子进程,与父进程同时执行。

  我们前面说过,进程 = 内核数据结构 + 可执行程序的代码和数据,而子进程能够执行父进程代码的原因,是因为 子进程被创建时,是以父进程为模版的子进程复制拷贝了父进程属性字段的大部分属性。

注意子进程虽然继承父进程的一些属性信息,但是像进程标识符(pid)等信息并不会复制父进程。并且代码只有一份,所以父子进程共享代码。


✈️再谈fork返回值

  前面我们说,fork之后的返回值有两个,通过上面的实验我们也可说明fork之后确实同时存在两个返回值。一个为0,一个为子进程pid。

  但是为什么给子进程返回0,父进程返回pid呢?

  一个父亲,可以有多个孩子,而每个孩子,都只有唯一的父亲。如果孩子去上学,生活费不够了,可以向同一个父亲要,而父亲也会准确的给缺钱的儿子发生活费。

  进程也是如此,因为子进程有多个,要想父进程准确无误的找到子进程就需要子进程的pid,而子进程只有一个父亲,并不需要返回特殊值。

  为什么fork会返回两次呢?

  我们从man手册里也查过了,fork是个函数,这个毋庸置疑,既然是函数,并且有类型,那么就有返回值,类似于:

pid_t fork()
{
  //...
  //...
  return id;
}

  我们在系统中执行程序,可执行程序变为进程,进程调用fork函数从而创建子进程,而fork函数内部,在return 之前,我们的子进程就已经创建完毕,最后return的只是id值。

  也就是说 在return返回之前,子进程已经创建出来了并且和父进程同时在执行,两个进程返回不同的id值也就能说的过去了!

  为什么接收fork的返回值的变量id既等于0,又大于0呢?

  我们之前说过,电脑里启动的一个个应用软件就是一个个进程,比如你再电脑登陆QQ、打开VS等应用,都是进程,那么如果你的QQ卡死了,会影响其他软件吗?并不会,所以在这里我们可以得到的结论是:每个进程都具有独立性,互不干扰!

  所以,如果一个进程创建了子进程,那么 随意杀掉一个进程,是不会影响另外一个进程的。我们不妨做一个实验:

1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 
  5 int main()
  6 {
  7     printf("before fork, porcess pid=%d, ppid=%d\n",getpid(), getppid());//进程创建之前的pid和ppid
  8     sleep(3);
  9     printf("start fork!\n");
 10     sleep(1);
 11 
 12     pid_t id = fork();//开始fork
 13 
 14     if(id < 0)
 15     {
 16         perror("fork fail!\n");
 17         return -1;
 18     }
 19 
 20     if(id == 0) {
 21         //子进程
 22         while(1)
 23         {
 24             printf("after fork, process pid=%d, ppid=%d\n", getpid(), getppid());
 25             sleep(1);
 26         }
 27     }
 28     else {
 29         //父进程
 30         while(1)                                                                                                                                                                                                                          
 31         {
 32             printf("after fork, process pid=%d, ppid=%d\n", getpid(), getppid());
 33             sleep(1);
 34         }
 35     }
 36 
 37     sleep(1);
 38     return 0;
 39 }

使用命令:

kill -9 进程标识符#杀死进程的信号

  从运行的结果来看,杀死一个进程确实不会影响另一个进程,就算是亲如父子的进程。

  而我想说明的是,既然父子进程各自独立,而他们又共享代码区数据区,那么如果一个进程需要更改数据时这不就间接影响了另一个进程吗?

  进程之间相互独立没有问题,当两个进程有一个需要修改原始数据时,子进程就会发生 写时拷贝Copy-On-Write ,以后会详谈),子进程将父进程的原始数据段复制下来,这样两个进程修改数据就不会影响彼此了。

  那么我们再考虑那个问题,fork为什么既可以是0又可以是别的数,fork在返回时返回的操作,就是在对变量进行写入,所以子进程会发生写时拷贝

  虽然他们的id(接收fork返回值的变量)都是同一个,但是数据却是不一样(这里有 虚拟地址 的知识,我们以后会讲),所以fork能既可以是0又可以是其他数就解释的通了。


📒✏️总结

  •   通过 系统调用接口 来获取父子进程的pid。
  •   运行起来的程序是 通过进程属性信息中的 cwd 来获取路径信息 的。
  •   创建子进程需要使用 fork函数接口,子进程会 继承父进程的部分属性字段,并且和父进程 共享代码段
  •   fork能返回两个不同的数给同一个变量靠的是 写时拷贝技术虚拟地址空间

  创作不易,如果能帮到你的话,还望能留下一个小小的三连呀~~

相关文章
|
1天前
|
弹性计算 运维 Shell
统计Linux 进程相关数量信息
【4月更文挑战第29天】
5 0
|
1天前
|
弹性计算 运维 Shell
统计 Linux 进程相关数量信息
【4月更文挑战第29天】
6 1
|
7天前
|
存储 Linux
【linux进程信号(二)】信号的保存,处理以及捕捉
【linux进程信号(二)】信号的保存,处理以及捕捉
|
7天前
|
NoSQL Linux 程序员
【linux进程信号(一)】信号的概念以及产生信号的方式
【linux进程信号(一)】信号的概念以及产生信号的方式
|
7天前
|
消息中间件 Linux
【linux进程间通信(二)】共享内存详解以及进程互斥概念
【linux进程间通信(二)】共享内存详解以及进程互斥概念
|
7天前
|
Linux
【linux进程间通信(一)】匿名管道和命名管道
【linux进程间通信(一)】匿名管道和命名管道
|
7天前
|
Java Shell Linux
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
|
7天前
|
存储 Linux C语言
【linux进程控制(一)】进程终止--如何干掉一个进程?
【linux进程控制(一)】进程终止--如何干掉一个进程?
|
7天前
|
安全 Linux 编译器
【linux进程(七)】程序地址空间深度剖析
【linux进程(七)】程序地址空间深度剖析
|
7天前
|
Shell Linux 程序员
【linux进程(六)】环境变量再理解&程序地址空间初认识
【linux进程(六)】环境变量再理解&程序地址空间初认识