目录

1.进程

1.1 进程概念和基本操作

1.1.1概念

1.1.1.1 进程是什么

1.1.1.2 为什么要有进程?

1.1.1.3  进程是怎么设计的

1.1.2 查看进程

1.1.2.1 /proc

1.1.2.2 指令

1.1.2.3 思维导图

1.1.3当前路径

1.1.3.1 更改工作路径

1.1.4 PCB:描述进程

1.1.4.1 概念

1.1.4.2 task_struct:PCB的一种(Linux中的PCB)

1.1.4.3 task_struct的分类

1.1.4.4task_struct的组织进程

1.1.4.5 思维导图

1.1.5 内存与结构体

1.2 父子进程

1.2.1  通过系统调用获取进程标示符

1.2.2 命令行直接启动时创建的对应的进程父进程都是bash

1.2.3 kill 9 [要杀掉的目标进程]

1.2.4 类似bash,它是如何创建的子进程呢

1.2.5 fork初识:通过系统调用创建进程

1.2.5.1 认识fork

1.2.5.2 关于fork的三个问题

1.2.5.3 fork演示

1.2.5.4 fork函数可以做到一次创建多个子进程

1.3 进程状态

1.3.1  task_struct不是属于双链表嘛?怎么能还属于调度队列呢?

1.3.2已有的条件的情况下,计算出结构体对象的起始地址,进而访问其他进程属性

1.3.3 task_struct既属于双链表又属于调度队列——内核为什么要这么做?

1.3.4 进程运行和进程阻塞

1.3.5 进程挂起

​编辑

1.3.6 结合Linux内核源代码,再看几种状态

1.3.7 进程状态查看命令

1.3.8 前后台问题

1.3.8.1 什么叫做前后台?为什么要有前后台?

1.3.8.2 为什么要有前后台

1.3.8.3 前后台的理解

1.3.9  T状态(stopped)和t(tracing stop)状态

1.3.10 D状态(disk sleep)

1.3.11 kill -l:查看Linux常见信号

1.3.12  gdb创建子进程,让子进程完成调试

1.3.13僵尸进程(Z)和死亡状态(X)

1.3.13.1 依旧小巧思

1.3.13.2 理论

1.3.13.3实验僵尸进程

1.3.13.4  僵尸进程的危害:内存泄漏问题

1.3.13.5 僵尸进程总结

1.3.14 孤儿进程

1.3.15进程状态总结


1.进程

1.1 进程概念和基本操作

1.1.1概念

1.1.1.1 进程是什么

一句话概括:当前:进程 = 内核数据结构(task_struct) + 自己的程序代码和数据

1.1.1.2 为什么要有进程?

1.1.1.3  进程是怎么设计的

我们可以用一个指令来查看进程ps axj | grep ./[可执行程序]——(这个命令以后经常使用)

1.1.2 查看进程

1.1.2.1 /proc

进程的信息可以通过/proc系统文件夹查看,比如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。

下面这些以数字命名的目录,这些数字本身是什么东西呢?这些数字就是进程对应的pid!即特定进程的pid。

我们来证明一下——比如下面这个父进程和子进程,我们可以获取一下pid分别为4027115的进程信息——

如下所示:ls /proc把进程内部的属性大部分也以文件的形式呈现出来了(Linux下一切皆文件),其实这些全都是内存的数据,包括像你把代码加载到内存,你的程序此时还在磁盘上,相当于在内存当中拷贝了一份,下图中上面反馈的属性大部分都是内存级的,简单理解一下——相当于查文件时,有些是磁盘上面的文件,有些是Linux操作系统对其做了封装(让我们以文件的形式看到了内核的数据)。蓝色字体是进程的目录。

1.1.2.2 指令

大多数进程信息同样可以使用topps这些用户级工具来获取。

ps命令是查进程的,它自己也是一个命令,它自己也有一个进程——

也可以问问万能的豆包,

信息不太好看,我们可以再添加一下条件——

剩下的选项我们到时候再了解,所以 ps命令也能够支持查看进程属性

我们现在已经认识了以上三个进程属性了。

  1 #include<stdio.h>
  2 #include <sys/types.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     while(1)
  8     {
  9         printf("hello,I am a process\n");
 10         sleep(1);
 11     }
 12     return 0;
 13 }  

使用top命令效果如下所示——

1.1.2.3 思维导图

cwd:属于进程的基本属性之一

1.1.3当前路径

cwd:current work dir(当前工作路径)。

为什么要有当前路径?为了支持进程访问文件时缺省路径的问题。

pwd这个命令我们都使用过,就叫做 查看当前所处的文件路径——也就是说,当前路径就是进程启动时候所处的路径

类似于这个工作:在你的当前路径下面创建了一个log.txt文件——

1.1.3.1 更改工作路径

我们可以用chdir(系统级函数,叫做 “更改当前系统的工作路径”)来更改工作路径(作用)

我们改掉文件的工作路径之后,再运行

此时我们ll,发现没有log.txt这个新建文件,我们再查看一下

log.txt果然在这个路径下。这不就证明了:文件建在哪个路径取决于当前进程的工作路径(cwd)

未来我们可以在指定的路径下创建文件,不指定就会在当前路径下新建文件。

我们再来看一个系统调用(库函数)getcwd:获取当前工作路径

修改一下代码,观察区别

换言之,既然能拿到ip地址,那就能够拿到工作路径。

这个路径下必然会有一个文件log.txt。

1.1.4 PCB:描述进程

1.1.4.1 概念

进程信息被放在一个叫做 进程控制块 的数据结构中,可以理解为进程属性的集合。

课本上称之为PCB(process control block),Linux操作系统下的PCB是:task_struct

1.1.4.2 task_struct:PCB的一种(Linux中的PCB)

在Linux中描述进程的结构体叫做task_struct

task_struct是Linux内核的一种数据结构类型,它会被装载到RAM(内存)里并且包含着进程的信息。

1.1.4.3 task_struct的分类

标示符:描述本进程的唯一标示符,用来区别其他进程,pid。

状态:任务状态,退出代码,退出信号等(就像投 简历也有状态)。

优先级:相对于其他进程的优先级。

程序计数器:程序中即将被执行的下一条指令的地址(和 进程切换 挂钩)。

内存指针(类比电话号码、邮箱):包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针(具有指向性,最难的)。

上下文数据:进程执行时处理器的寄存器中的数据【休学的例子,要加图CPU,寄存器】(取数据——方便下次恢复出来——面试官生成面试报告[ 临时数据 ],给“我”打分,一面二面三面,方便下一轮的面试官获得上一轮的面试官的数据)。

IO状态信息:包括显示的I/O请求,分配给进程的I / O设备和被进程使用的文件列表。

记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等(就如投简历面试,面试面了几轮?面了多久?)。

以及其他信息。我们后面会介绍,我们下面先介绍标识符pid。

1.1.4.4task_struct的组织进程

我们可以在内核源代码里找到它(这里不演示了)。所有运行在系统里的进程都以task_struct双链表的形式存在内核里。

我们以前在数据结构学过的双链表如下所示,但是PCB这里的双链表是没有数据(data)的,这就是Linux内核中链表的设计,和我们以前实现的链表数据结构还是有区别的。

1.1.4.5 思维导图

1.1.5 内存与结构体

程序从磁盘加载到内存中

做个实验

1.2 父子进程

两个死循环同时跑——两个进程(以前C语言时是单执行过程),意味着:未来同一个代码,两个进程做不同的事情。

如下图所示:子进程返回 0 执行 if,父进程返回 pid 执行 else if——

我们查看Linux的内核源代码,可以看到父进程也是会记录下pid的——

如果要访问父进程的pid(即ppid),我们还有这样一个系统调用:getppid——

1.2.1  通过系统调用获取进程标示符

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
 printf("pid: %d\n", getpid());
 printf("ppid: %d\n", getppid());
 return 0;
}

1.2.2 命令行直接启动时创建的对应的进程父进程都是bash

此时我们发现,每次运行,pid都不同,但是ppid保持不变

我们ps查看一下,此时一个叫28750的进程映入眼帘,这个进程叫做“bash”

我们自己bash一下再用ps去查的时候会发现多了一个bash

bash(死循环的一个软件):代码内部死循环的,不退出——要退出需要按Ctrl + D退出!

-bash:命令行直接启动时,创建的对应的进程,父进程都是bash(命令行解释器)。

-:bash前面带“-”的代表它是 远程登录进来 的。

who(谁在登录?谁在连接?)命令

每一次进来时,系统都会自动分配一个bash。

1.2.3 kill 9 [要杀掉的目标进程]

怎么干掉bash呢?我们可以用到这样一个命令:kill 9 [要杀掉的目标进程]。

这也是bash所担心的风险——一旦“杀掉”,机器就挂掉了——

kill 命令格式:kill -9 [PID] 或 kill -SIGKILL [PID]

我们可以先找到PID再杀掉目标进程——

1.2.4 类似bash,它是如何创建的子进程呢

相当于整个的代码是由父进程提供的——

1.2.5 fork初识:通过系统调用创建进程

1.2.5.1 认识fork

1.2.5.2 关于fork的三个问题

(1)fork有两个返回值

(2)父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。

问题1:为什么给子进程返回0,给父进程返回子进程的pid?


在数量上,一个父进程和它的子进程的比例是1 : n。父进程需要通过唯一值pid标识不同的子进程,并且根据pid来控制子进程,给父进程返回子进程的pid也是为了告诉父进程多了一个子进程,让它记住子进程的pid,未来用这个pid来控制子进程。

这里子进程返回0说明一件事:子进程被成功创建出来了。
问题2:为什么同一个函数,会返回两次?

走到return之前,子进程就已经被创建了(这是 return两次 核心要点)——父子进程各自return了一次,这样就是两次。

问题3:为什么同一个变量id,既满足==0,又满足大于0?

进程具有很强的独立性——任何一个进程崩溃都不会影响到另一个进程(就像微信程序奔溃了,你的QQ会不会崩溃?)

父子进程牵扯到浅拷贝(指针)的问题,有一些共享的情况很正常。

我们再用%p打印一下flag的地址

现象:上面这个父进程和子进程的全局变量flag地址竟然是一样的!可是内容明明不一样啊?

所以这个地址一定不是物理地址!如果是物理地址,就不可能一个是10(flag不做修改的父进程),一个是11、12……(flag修改的子进程),说明不是物理地址而是虚拟地址。

子进程有很多属性继承自父进程(如路径),但个别属性和父进程区别开来了(如pid)——总之,子进程以父进程为模版。

父进程也是从它的父进程(bash)那里继承来的。

代码和数据是从磁盘加载到内存的,子进程也会指向父进程的数据和代码。

从上图中我们可以得出关于父进程和子进程的两个重要结论
 

父子同时读,内容竟然不同!同一个地址值!因此,绝不可能是物理内存的地址!那是什么?虚拟地址

写时拷贝——父子数据分开所采用的机制

这里是经过虚拟内存机制映射之后,实际到不同的内存空间去了。

这样初步解释了“问题3:为什么同一个变量id,既满足==0,又满足大于0?”这个问题。

这里 fork 的返回值写入到id,说明 返回的本质就是写入

1.2.5.3 fork演示
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
 int ret = fork();
 printf("hello proc : %d!, ret: %d\n", getpid(), ret);
 sleep(1);
 return 0;
}

fork之后通常要用if进行分流——

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
 int ret = fork();
 if(ret < 0){
 perror("fork");
 return 1;
 }
 else if(ret == 0){ //child
 printf("I am child : %d!, ret: %d\n", getpid(), ret);
 }else{ //father
 printf("I am father : %d!, ret: %d\n", getpid(), ret);
 }
 sleep(1);
 return 0;
}
1.2.5.4 fork函数可以做到一次创建多个子进程

我们封装一个Loop函数,里面执行打印子进程和父进程的id的功能

在for循环里面执行10次,运行结果如下所示

子进程一进去就陷入循环:

实验结果证明:fork函数确实可以做到一次创建多个子进程。

1.3 进程状态

1.3.1  task_struct不是属于双链表嘛?怎么能还属于调度队列呢?

内存地址是从低地址到高地址线性排列的——几乎所有连续存储的数据结构在内存中都是从低地址向高地址排列的。不同类型的存储区域在这个线性空间中的分布方式不同。大部分数据在内存中的存储顺序是“低地址指向高地址”。

高地址 0xFFFFFFFF
┌─────────────────┐
│   内核空间      │ ← 用户进程不可访问
├─────────────────┤
│   栈(stack)     │ ← 向下增长(向低地址)
│        ↓        │
├─────────────────┤
│        ↑        │
│   堆(heap)      │ ← 向上增长(向高地址)
├─────────────────┤
│   未初始化数据  │
│   (BSS段)       │
├─────────────────┤
│   已初始化数据  │
│   (数据段)      │
├─────────────────┤
│   代码段        │
│   (text段)      │
└─────────────────┘
低地址 0x00000000

以整型值为例——

以数组为例

结构体

1.3.2已有的条件的情况下,计算出结构体对象的起始地址,进而访问其他进程属性

已知struct内部元素的地址,反求结构体变量的起始地址——

1.3.3 task_struct既属于双链表又属于调度队列——内核为什么要这么做?

1.3.4 进程运行和进程阻塞

1.3.5 进程挂起

1.3.6 结合Linux内核源代码,再看几种状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态:一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。下面的状态在kernel源代码里定义:

/*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/
static const char *const task_state_array[] = {
	 "R (running)", /*0 */
	 "S (sleeping)", /*1 */
	 "D (disk sleep)", /*2 */
	 "T (stopped)", /*4 */
	 "t (tracing stop)", /*8 */
	 "X (dead)", /*16 */
	 "Z (zombie)", /*32 */
};

R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 [interruptible sleep])。

D磁盘休眠状态(Disksleep)有时候也叫不可中断睡眠状态(uninterruptiblesleep),在这个状态的进程通常会等待IO的结束。

T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

1.3.7 进程状态查看命令

ps aux / ps axj 命令
a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等

为了方便演示查看进程状态的命令,我们简单写一下这样一个代码—

1 #include<stdio.h>
2
3 int main()                                                                                                                                                                                  
4 {
5     int a = 0;
6     scanf("%d",&a);
7     printf("a=%d\n",a);
8 
9     return 0;
10 }

我们写一下对应的Makefile——

  7 myproc.exe:myproc.c
  8     gcc -o $@ $^ -g
  9 .PHONY:clean
 10 clean:
 11     rm -f myproc.exe 

如下图所示,此时的这个状态就是我们前面提到的阻塞状态

我们再来写一段代码,打印“我是一个进程以及pid”,

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5     while(1)
  6     {
  7         printf("I am a process,pid: %d\n",getpid());
  8         printf("I am a process,pid: %d\n",getpid());
  9         printf("I am a process,pid: %d\n",getpid());
 10         printf("I am a process,pid: %d\n",getpid());
 11         printf("I am a process,pid: %d\n",getpid());
 12         printf("I am a process,pid: %d\n",getpid());                                       
 13         sleep(1);
 14     }
 15     return 0;
 16 }

1.3.8 前后台问题

1.3.8.1 什么叫做前后台?为什么要有前后台?

1.3.8.2 为什么要有前后台

1.3.8.3 前后台的理解

1.3.9  T状态(stopped)和t(tracing stop)状态

T状态:stopped(非法、越权时,OS不想杀掉这个进程,就暂停了)。

T、t状态在进程上没有本质区别。

我们可以通过在gdb里面 打断点 的方式来观察暂停状态。

1.3.10 D状态(disk sleep)

依旧来讲一个小巧思来理解

1.3.11 kill -l:查看Linux常见信号

1.3.12  gdb创建子进程,让子进程完成调试

cgdb底层也是gdb,这里建议用gdb是因为cgdb有半图形化界面,挡着看着不舒服。

gdb(cgdb本质也是gdb)——

1.3.13僵尸进程(Z)和死亡状态(X)

1.3.13.1 依旧小巧思

X(dead):死亡状态会释放代码和数据,但是实际上没有直接——进程会进入到僵尸状态(Zombie,僵尸模式)。

这里有个小故事:假设有个头发稀疏的三十多岁的程序员正在街上跑步,忽然人身一歪影一斜,就死在街上了,路人看到后赶紧拨打了110,但是警察不会直接通知家属把人拉走,在排除他杀之后(给社会层面一个交待),才会再叫家属把人拉走。

1.3.13.2 理论

(1)僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死 / 尸进程。

(2)僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。

(3)所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

1.3.13.3实验僵尸进程
#include <stdio.h>
#include <stdlib.h>

int main()
{
	  pid_t id = fork();
	  if(id < 0){
	  	perror("fork");
	  	return 1;
	  }
	  else if(id > 0){ //parent
	  	printf("parent[%d] is sleeping...\n", getpid());
	  	sleep(30);
	  }
	  else{
	  	printf("child[%d] is begin Z...\n", getpid());
	  	sleep(5);
	  	exit(EXIT_SUCCESS);
	  }
	  return 0;
}

编译并在另一个终端下启动监控 

1.3.13.4  僵尸进程的危害:内存泄漏问题

进程的退出状态必须被维持下去,因为它要告诉关心它的进程(父进程)——你交给我的任务,我办的怎么样了(父进程需要知道——读取进程task_struct内部的退出信息)。父进程如果一直不读取,那子进程是不是就一直处于Z状态?是的,这会存在很大的风险问题。

维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,是不是Z状态一直不退出,PCB就得一直维护?没错,Z状态一直不退出,PCB就必须一直维护。

那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

会内存泄漏嘛?是,数据没释放,PCB一直维持,占据了内存空间资源。
 

1.3.13.5 僵尸进程总结

1.3.14 孤儿进程

父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?

父进程先退出,子进程就称之为“孤儿进程”。

孤儿进程被1号init / systemd进程领养,当然要有init / systemd进程回收喽。

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

int main()
{
	 pid_t id = fork();
	 if(id < 0){
	 	perror("fork");
	 	return 1;
	 }
 	else if(id == 0){//child
 		printf("I am child, pid : %d\n", getpid());
 		sleep(10);
 }else{
 	 //parent
	 	printf("I am parent, pid: %d\n", getpid());
	 	sleep(3);
	 	exit(0);
	 }
	 return 0;
}

父进程先退出,子进程还在,这就是 “孤儿进程”——

1.3.15进程状态总结

整个的进程诞生、管理与消亡的过程如下图所示

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐