目录
- ⛹🏽进程简介
- ⛹🏽查看进程
- ⛹🏽系统调用
- 🚴🏽获取进程标示符
- 🚴🏽创建进程
- ⛹🏽进程状态
- 🚴🏽孤儿进程:
- 🚴🏽进程优先级
- ⛹🏽环境变量
- 🚴🏽查看环境变量
- 🚴🏽添加环境变量
- 🚴🏽组织环境变量
- 🚴🏽获取环境变量
- 🚴🏽环境变量特性
- ⛹🏽进程地址空间
- ⛹🏽总结
⛹🏽进程简介
进程概念:程序的一个执行实例或者一个担当分配系统资源(CPU时间,内存)的实体。
进程描述:进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,简称为PCB(process control block),在Linux操作系统下的PCB是: task_struct
。task_struct
是PCB的一种,task_struct
是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct
内容分类:
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
组织进程:在内核源代码里,所有运行在系统里的进程都以task_struct
链表的形式存在内核里。
⛹🏽查看进程
进程的信息可以通过 /proc
系统文件夹查看,如下图:
大多数进程信息同样可以使用ps这些用户级工具来获取,例如我们也可以写一个程序运行然后使用ps命令来查看该进程,如下代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1){
printf("正在运行\n");
sleep(1);
}
return 0;
}
编译运行,然后使用命令ps axj | grep test1 | grep -v grep
查看进程。
⛹🏽系统调用
🚴🏽获取进程标示符
进程的标识符id称为PID,进程的父进程标识符id成为PPID。
系统给我们分别提供了获取PID和PPID的函数——getpid()
和getppid()
。
通过查看手册可以看到,使用这两个函数我们需要包含sys/types.h
和unistd.h
这两个头文件,并且getpid
返回当前的进程id,getppid
返回当前进程的父进程的id。
我们可以在自己的代码中使用系统提供给我们的两种系统调用函数来获取进程的PID和PPID,如下代码:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 int main()
5 {
6 while(1){
7 printf("正在运行,我的PID为:%d,我的PPID为:%d\n", getpid(), getppid());
8 sleep(1);
9 }
10 return 0;
11 }
我们还可以用ps命令来查看进程,如下图:
🚴🏽创建进程
在系统接口中,还给我们提供了创建子进程的函数——fork()
。
查看手册可以看到,使用fork函数我们需要包含unistd.h
这个头文件,当子进程成功创建时,将子进程的id返回给他的父亲,返回0给他自己,如果创建失败则返回-1给他的父亲。
如下代码:
22 int main()
23 {
24 int ret = fork();
25 if(ret > 0)
26 {
27 while(1)
28 {
29 printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());
30 sleep(1);
31 }
32 }
33 else if (ret == 0)
34 {
35 while(1)
36 {
37 printf("我是子进程,pid:%d, ppid: %d\n", getpid(), getppid());
38 sleep(1);
39 }
40 }
41 else{
42 printf("error\n");
43 }
44 return 0;
45 }
由结果可以看到,子进程的PPID和父进程的PID一样。可能你会觉得这个结果很奇怪,为什么只有一个ret,但是却把if和else都执行了,这是因为我们使用fork创建子进程后,代码就是父子进程共享,数据各自开辟空间,私有一份,在返回时发生写时拷贝。
如下代码:
21 int main()
22 {
23 printf("aaaaaaaaaaaaaaaaaa\n");
24 fork();
25 printf("bbbbbbbbbbbbbbbbb\n");
26 return 0;
27 }
⛹🏽进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有如下几种状态:
R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,在任务列表里看看不到这个状态。
Z(zombie)-僵尸进程(僵死状态(Zombies)):是一个比较特殊的状态。
当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程,僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
我们可以用代码来观察僵尸进程,如下代码:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5 int main()
6 {
7 int ret = fork();
8 if(ret == 0)
9 {
10 for(int i = 5; i > 0; i--)
11 {
12 printf("我是子进程%d,我的父进程%d,结束还有%d秒\n", getpid(), getppid(), i);
13 sleep(1);
14 }
15 }
16 else if (ret > 0)
17 {
18 for(int i = 10; i > 0; i--)
19 {
20 printf("我是父进程%d,我的子进程%d,结束还有%d秒\n", getpid(), getppid(), i);
21 sleep(1);
22 }
23 }
24 else{
25 printf("eroor\n");
26 }
27 return 0;
28 }
编译运行该程序后我们可以在xshell上运行ps命令来观察进程状态.
命令:
while :; do ps axj | head -1 && ps axj | grep test1 | grep -v grep;sleep 1;echo "===============================";done
如上图我们可以看出,当子进程退出的时候,他的状态由s(sleep)转换为z(zombie)。
僵尸进程危害:
进程的退出状态必须被维持下去,因为他要告诉它的父进程。可父进程如果一直不读取,那子进程就一直处于僵尸状态。
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,也就是说,如果僵尸状态一直不退出,PCB一直都要维护!
那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间! 就会造成内存泄漏。
🚴🏽孤儿进程:
当我们的父进程如果提前退出,那么子进程后退出,在子进程进入僵尸进程之后,那该如何处理呢?
我们称这种父进程先退出,子进程后退出,把这种子进程就称之为“孤儿进程”
这种孤儿进程通常是被1号init进程回收领养
我们也可以用一段代码来观察,如下代码:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5 int main()
6 {
7 int ret = fork();
8 if(ret == 0)
9 {
10 for(int i = 10; i > 0; i--)
11 {
12 printf("我是子进程%d,我的父进程%d,结束还有%d秒\n", getpid(), getppid(), i);
13 sleep(1);
14 }
15 }
16 else if (ret > 0)
17 {
18 for(int i = 5; i > 0; i--)
19 {
20 printf("我是父进程%d,我的子进程%d,结束还有%d秒\n", getpid(), getppid(), i);
21 sleep(1);
22 }
23 }
24 else{
25 printf("eroor\n");
26 }
27 return 0;
28 }
用ps命令来查看进程状态
while :; do ps axj | head -1 && ps axj | grep test1 | grep -v grep;sleep 1;echo "===============================";done
如上图我们可以看到,当父进程退出后,我们的子进程就会更换自己的父进程。
🚴🏽进程优先级
cpu资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行权利。
配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上。
这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程:
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容
我们可以看到有如下几个重要信息
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
其中,PRI就是进程的优先级,通俗点说就是程序被CPU执行的先后顺序,PRI值越小进程的优先级别越高。
NI就是nice值,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,在加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序的优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值,nice值取值范围是-20至19,一共40个级别。
需要注意的一点是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正修正数据
查看进程优先级的命令:top
用top命令更改已存在进程的nice,进入top后按“r”然后输入进程PID再输入nice值。
如上图,当我们将test1的进程NI值修改为10之后,我们可以看到test1的PRI值变为30。
⛹🏽环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见环境变量:
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash。
🚴🏽查看环境变量
命令:echo $NAME
NAME为你的环境变量名称,例如echo $USER
🚴🏽添加环境变量
我们都知道,当我们编译好一份代码文件时,如果我们想运行这个代码文件,我们需要在前面加上./
,这个./
是在当前路径下查找你所要执行的文件。
但是为什么有些指令可以直接执行,不需要带路径,而我们的二进制程序需要带路径才能执行?
因为这些指令在环境变量中存有他的路径,当我们执行这些指令时,会在环境变量所保存的路径中去查找该指令。
我们可以用命令echo $PATH
来查看我们的环境变量,如下图:
我们可以将我们的程序所在路径加入环境变量PATH当中,然后查看与之前有什么不同。将程序路径加入到环境变量中我们可以使用命令:
export PATH=$PATH:[我们文件所在的路径]
例如当前有这么一个.c
文件,如下代码:
1 #include <stdio.h>
2
3 int main()
4 {
5 for(int i = 0; i < 5; i++)
6 {
7 printf("hello world\n");
8 }
9 return 0;
10 }
我们当前所处路径为/home/wzh/linux/Test23_4_21
我们将该路径加入到PATH环境变量中去,执行命令export PATH=$PATH:/home/wzh/linux/Test23_4_21
之后我们可以发现,我们不需要带./
也是可以运行我们的程序,如下图:
🚴🏽组织环境变量
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以\0
结尾的环境字符串
🚴🏽获取环境变量
我们可以通过代码来获取环境变量,例如我们可以通过main函数的第三个参数来获取,如下代码:
1 #include <stdio.h>
2
3 int main(int argc, char* argv[], char* env[])
4 {
5 for(int i = 0; env[i]; i++)
6 {
7 printf("%s\n", env[i]);
8 }
9
10 return 0;
11 }
除了参数获取,我们还可以通过第三方变量environ获取,如下代码
16 #include <stdio.h>
17 int main(int argc, char* argv[])
18 {
19 extern char** environ;
20 for(int i = 0; environ[i]; i++)
21 printf("%s\n", environ[i]);
22 return 0;
23 }
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
除了使用代码参数和变量查看我们的环境变量,我们还可以通过系统调用接口来获取环境变量,系统给我们提供了一个getenv()
函数,如下图:
我们可以看出,这个函数是包含在stdlib.h
这个头文件中,因此我们在使用这个函数的时候还得包这个头文件,如下代码:
26 #include <stdio.h>
27 #include <stdlib.h>
28 int main()
29 {
30 printf("%s\n", getenv("PATH"));
31 return 0;
32 }
🚴🏽环境变量特性
环境变量通常是具有全局属性,可以被子进程继承下去。如下代码:
26 #include <stdio.h>
27 #include <stdlib.h>
28 int main()
29 {
31 char* ret = getenv("testenv");
32 if(ret)
33 printf("%s\n", ret);
34 return 0;
35 }
当我们编译运行的时候发现什么都没有打印出来,说明不存在这个环境变量,但是当我们用命令export testenv="hello world"
的时候再次运行会发现这个时候就打印出来hello world。
⛹🏽进程地址空间
在我们基于32位平台下,我们知道有如下图所示的地址空间分布
但是有这么一份奇怪的代码:
40 #include <sys/types.h>
41 #include <stdio.h>
42 #include <unistd.h>
43 #include <stdlib.h>
44 int main()
45 {
46 int val = 0;
47 pid_t ret = fork();
48 if(ret == 0)
49 {
50 for(int i = 0; i < 5; i++)
51 {
52 val++;
53 printf("child : %d childpid : %d address : %p\n", val, getpid(), &val);
54 sleep(1);
55 }
56 }
57 else if (ret > 0)
58 {
59 for(int i = 0; i < 5; i++)
60 {
61 val += 10;
62 printf("father : %d fatherpid : %d address : %p\n", val, getpid(), &val);
63 sleep(1);
64 }
65 }
66 else{
67 perror("fork");
68 }
69 return 0;
70 }
在我们运行后发现结果如图所示
我们都知道进程与进程之间是独立的,所以子进程和父进程的val值修改互相并不影响,但是我们观察运行结果我们可以看到,子进程和父进程的val值地址都是一样的,如果它们地址是一样的那么父子进程的val就应该是同一个val,可是如果是同一个val那么他们修改又怎么会互不影响呢。所以我们大概就可以得出下面的结论:
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
该地址绝对不是物理地址
这样的地址在Linux下,我们把这种地址叫做虚拟地址,我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址用户一概看不到,由操作系统统一管理,操作系统必须负责将虚拟地址转化成物理地址 。因此我们所说的程序地址空间严格的说是不准确的,应该叫进程地址空间。
当我们的程序编译运行变为一个进程的时候,其中PCB(task_struct)中的某个指针就会指向一块虚拟地址空间,而虚拟地址空间又会由页表进行映射到物理内存上,如下图:
⛹🏽总结
本篇博客介绍了Linux的一些进程状态,环境变量,以及进程地址空间和一些系统调用接口,用代码来看现象。如果觉得本篇博客对你有帮助的话就三连吧!另外由于作者水平有限,如果文中有什么纰漏或者错误还请指正。谢谢大家。