Linux 进程概念
一.基本概念
课本概念: 程序的一个执行实例,正在执行的程序等。
内核观点: 担当分配系统资源(CPU时间,内存)的实体。
只要写过代码的都知道,当你的代码进行编译链接后便会生成一个可执行程序,这个可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程。
描述进程PCB
当系统中出现大量进程时,用ps aux可以显示系统中当前存在的进程
而当你开机的时候启动的第一个程序就是我们的操作系统,我们都知道操作系统是做管理工作的,而其中就包括了进程管理。而系统内是存在大量进程的,那么操作系统是如何对进程进行管理的呢?
管理的六字真言:先描述,再组织。操作系统管理进程也是一样的,操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的,当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block)。
类似一个双向链表,只要拿到这个双链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条双链表的一系列操作。
总的来说,操作系统对进程的管理实际上就变成了对该双链表的增、删、查、改等操作。
task_struct PCB的一种
进程控制块(PCB)是描述进程的,在C++当中我们称之为面向对象,而在C语言当中我们称之为结构体,既然Linux操作系统是用C语言进行编写的,那么Linux当中的进程控制块必定是用结构体来实现的。
- PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息。
task_struct内容分类
task_struct就是Linux当中的进程控制块,task_struct当中主要包含以下信息:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级
- 程序计数器(pc): 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
二.查看进程
2.1通过目录查看
ls /proc //代码来查看
2.2通过ps来查看
ps aux
ps命令与grep命令搭配使用,即可只显示某一进程的信息。
ps aux | head -1 && ps aux | grep proc | grep -v grep
2.3通过系统调用进程PID和PPID
通过使用系统调用函数,getpid和getppid即可分别获取进程的PID和PPID。
我们可以通过一段代码来进行测试。
程序运行后循环打印PID 和PPID
这里可以发现通过ps命令得到的进程的PID和PPID与使用系统调用函数getpid和getppid所获取的值相同。
2.4通过系统调用创建进程- fork初始
2.4.1.fork函数创建子进程
fork是一个系统调用级别的函数,其功能就是创建一个子进程
若是代码当中没有fork函数,我们都知道代码的运行结果就是循环打印该进程的PID和PPID。而加入了fork函数后,代码运行结果如下:
我们这边可以发现第一行数据是该进程的PID和PPID,第二行数据是代码中fork函数创建的子进程的PID和PPID。我们可以发现fork函数创建的进程的PPID就是proc进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系。
我们知道加载到内存当中的代码和数据是属于父进程的,那么fork函数创建的子进程的代码和数据又从何而来呢?
我们看看以下代码的运行结果:
若是代码当中没有fork函数,我们都知道代码的运行结果就是循环打印该进程的PID和PPID。而加入了fork函数后,代码运行结果如下:
运行结果是循环打印两行数据,第一行数据是该进程的PID和PPID,第二行数据是代码中fork函数创建的子进程的PID和PPID。我们可以发现fork函数创建的进程的PPID就是proc进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系。
我们知道加载到内存当中的代码和数据是属于父进程的,那么fork函数创建的子进程的代码和数据又从何而来呢?
实际上,使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码,则默认情况下父子进程都可以执行。需要注意的是,父子进程虽然代码共享,但是父子进程的数据各自开辟空间(采用写时拷贝)。
tips:使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现。
2.4.2使用if分流
上面说到,fork函数创建出来的子进程与其父进程共同使用一份代码,但我们如果真的让父子进程做相同的事情,那么创建子进程就没有什么意义了。
实际上,在fork之后我们通常使用if语句进行分流,即让父进程和子进程做不同的事。
fork函数的返回值:
1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0。
2、如果子进程创建失败,则在父进程中返回 -1。
既然父进程和子进程获取到fork函数的返回值不同,那么我们就可以据此来让父子进程执行不同的代码,从而做不同的事。
fork创建出子进程后,子进程会进入if的语句循环打印,而父进程会进入else if的语句循环打印
三.进程状态
一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器,有时虽有空闲处理器但因等待某个时间的发生而无法执行,这一切都说明进程和程序不相同,进程是活动的且有状态变化的,于是就有了进程状态这一概念。
这里我们具体谈一下Linux操作系统中的进程状态,Linux操作系统的源代码当中对于进程状态有如下定义:
/** 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 *task_state_array[] = {\"R (running)\", /* 0*/ \"S (sleeping)\", /* 1*/ \"D (disk sleep)\", /* 2*/ \"T (stopped)\", /* 4*/ \"T (tracing stop)\", /* 8*/ \"Z (zombie)\", /* 16*/ \"X (dead)\" /* 32*/};
tips:
• R运⾏状态(running): 并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏队列⾥。 • S睡眠状态(sleeping): 意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。 • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。 • T停⽌状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运⾏。 • X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。在Linux操作系统当中我们可以通过 ps aux 或 ps axj 命令查看进程的状态。
ps aux
3.1僵尸进程
前面说到,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,我们就称之为僵尸进程。
例如,对于以下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。也就是说,子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么此时子进程就进入了僵尸状态。
#include #include #include int main(){printf(\"I am running...\\n\");pid_t id = fork();if(id == 0){ //childint count = 5;while(count){printf(\"I am child...PID:%d, PPID:%d, count:%d\\n\", getpid(), getppid(), count);sleep(1);count--;}printf(\"child quit...\\n\");exit(1);}else if(id > 0){ //fatherwhile(1){printf(\"I am father...PID:%d, PPID:%d\\n\", getpid(), getppid());sleep(1);}}else{ //fork error}return 0;}
运行该代码后,我们可以通过以下监控脚本,每隔一秒对该进程的信息进行检测。
while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo \"######################\";sleep 1;done
检测后可以发现,当子进程退出后,子进程的状态变成僵尸状态。
僵尸进程的危害
• 进程的退出状态必须被维持下去,因为他要告诉关⼼它的进程(⽗进程),你交给我的任务,我 办的怎么样了。可⽗进程如果⼀直不读取,那⼦进程就⼀直处于Z状态?是的! • 维护退出状态本⾝就是要⽤数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中, 换句话说,Z状态⼀直不退出,PCB⼀直都要维护?是的! • 那⼀个⽗进程创建了很多⼦进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数 据结构对象本⾝就要占⽤内存,想想C中定义⼀个结构体变量(对象),是要在内存的某个位置 进⾏开辟空间!• 内存泄漏?是的! • 如何避免?后⾯讲
⾄此,值得关注的进程状态全部讲解完成,下⾯来认识另⼀种进程
3.2孤儿进程
• ⽗进程如果提前退出,那么⼦进程后退出,进⼊Z之后,那该如何处理呢? • ⽗进程先退出,⼦进程就称之为“孤⼉进程” • 孤⼉进程被1号init进程领养,当然要有init进程回收喽。
#include #include #include int main(){pid_t id = fork();if(id < 0){perror(\"fork\");return 1;}else if(id == 0){//childprintf(\"I am child, pid : %d\\n\", getpid());sleep(10);}else{//parentprintf(\"I am parent, pid: %d\\n\", getpid());sleep(3);exit(0);}return 0;}
观察代码运行结果,在父进程未退出时,子进程的PPID就是父进程的PID,而当父进程退出后,子进程的PPID就变成了1,即子进程被1号进程领养了。
四.进程优先级
4.1基本概念
什么是优先级?
优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。
优先级存在的原因?
优先级存在的主要原因就是资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。
4.2查看系统进程
在Linux或者Unix操作系统中,用ps -l命令会类似输出以下几个内容:
ps -l
列出的信息当中有几个重要的信息,如下:
- UID:代表执行者的身份。
- PID:代表这个进程的代号。
- PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行。
- NI:代表这个进程的nice值。
4.3 PRI与NI
1. PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高。
2. NI代表的是nice值,其表示进程可被执行的优先级的修正数值。
3.PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。
4. 若NI值为负值,那么该进程的PRI将变小,即其优先级会变高。
5.调整进程优先级,在Linux下,就是调整进程的nice值。
6.NI的取值范围是-20至19,一共40个级别。
注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。
4.4查看进程优先级信息
当我们创建一个进程后,我们可以使用ps -al命令查看该进程优先级的信息。
注意: 在Linux操作系统中,初始进程一般优先级PRI默认为80,NI默认为0。
4.5 通过top命令更改进程的nice值
top命令就相当于Windows操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况。
4.6四个重要概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
五.环境变量
• 环境变量(environment variables)⼀般是指在操作系统中⽤来指定操作系统运⾏环境的⼀些参数 • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪 ⾥,但是照样可以链接成功,⽣成可执⾏程序,原因就是有相关环境变量帮助编译器进⾏查找。 • 环境变量通常具有某些特殊⽤途,还有在系统当中通常具有全局特性
5.1 常见的环境变量
• PATH : 指定命令的搜索路径 • HOME : 指定⽤⼾的主⼯作⽬录(即⽤⼾登陆到Linux系统中时,默认的⽬录) • SHELL : 当前Shell,它的值通常是/bin/bash。
5.2 查看环境变量的方法
echo $NAME //NAME:你的环境变量名称例如:测试PATH 大家有没有想过这样一个问题:为什么执行ls命令的时候不用带./就可以执行,而我们自己生成的可执行程序必须要在前面带上./才可以执行?
容易理解的是,要执行一个可执行程序必须要先找到它在哪里,既然不带./就可以执行ls命令,说明系统能够通过ls名称找到ls的位置,而系统是无法找到我们自己的可执行程序的,所以我们必须带上./,以此告诉系统该可执行程序位于当前目录下。
而系统就是通过环境变量PATH来找到ls命令的,查看环境变量PATH我们可以看到如下内容:
可以看到环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令时,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径当中进行查找。
而ls命令实际就位于PATH当中的某一个路径下,所以就算ls命令不带路径执行,系统也是能够找到的。
5.3测试HOME
任何一个用户在运行系统登录时都有自己的主工作目录(家目录),环境变量HOME当中即保存的该用户的主工作目录。
超级用户实例:
5.4测试SHELL
我们在Linux操作系统当中所敲的各种命令,实际上需要由命令行解释器进行解释,而在Linux当中有许多种命令行解释器(例如bash、sh),我们可以通过查看环境变量SHELL来知道自己当前所用的命令行解释器的种类。
部分环境变量说明:
5.5环境变量的组织方式
每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以’\\0’结尾的环境字符串,最后一个字符指针为空。
通过代码获取环境变量
你知道main函数其实是有参数的吗?
main函数其实有三个参数,只是我们平时基本不用它们,所以一般情况下都没有写出来。
我们可以在Windows下的编译器进行验证,当我们调试代码的时候,若是一直使用逐步调试,那么最终会来到调用main函数的地方。
在这里我们可以看到,调用main函数时给main函数传递了三个参数。
、
现在我们来说说main函数的前两个参数,main函数的第二个参数是一个字符指针数组,数组当中的第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。
5.6通过系统调用获取环境变量
除了通过main函数的第三个参数和第三方变量environ来获取环境变量外,我们还可以通过系统调用getenv函数来获取环境变量。
getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。
例如,使用getenv函数获取环境变量PATH的值。
六.程序地址空间
在Linux操作系统中,我们可以通过以下代码对该布局图进行验证:
#include #include #include int g_unval;int g_val = 100;int main(int argc, char *argv[], char *env[]){const char *str = \"helloworld\";printf(\"code addr: %p\\n\", main);printf(\"init global addr: %p\\n\", &g_val);printf(\"uninit global addr: %p\\n\", &g_unval);static int test = 10;char *heap_mem = (char*)malloc(10);char *heap_mem1 = (char*)malloc(10);char *heap_mem2 = (char*)malloc(10);char *heap_mem3 = (char*)malloc(10);printf(\"heap addr: %p\\n\", heap_mem); //heap_mem(0), &heap_mem(1)printf(\"heap addr: %p\\n\", heap_mem1); //heap_mem(0), &heap_mem(1)printf(\"heap addr: %p\\n\", heap_mem2); //heap_mem(0), &heap_mem(1)printf(\"heap addr: %p\\n\", heap_mem3); //heap_mem(0), &heap_mem(1)printf(\"test static addr: %p\\n\", &test); //heap_mem(0), &heap_mem(1)printf(\"stack addr: %p\\n\", &heap_mem); //heap_mem(0), &heap_mem(1)printf(\"stack addr: %p\\n\", &heap_mem1); //heap_mem(0), &heap_mem(1)printf(\"stack addr: %p\\n\", &heap_mem2); //heap_mem(0), &heap_mem(1)printf(\"stack addr: %p\\n\", &heap_mem3); //heap_mem(0), &heap_mem(1)printf(\"read only string addr: %p\\n\", str);for(int i = 0 ;i < argc; i++){ printf(\"argv[%d]: %p\\n\", i, argv[i]);} for(int i = 0; env[i]; i++){ printf(\"env[%d]: %p\\n\", i, env[i]);} return 0;}
运行结果:
$ ./a.out2 code addr: 0x40055d3 init global addr: 0x6010344 uninit global addr: 0x6010405 heap addr: 0x17910106 heap addr: 0x17910307 heap addr: 0x17910508 heap addr: 0x17910709 test static addr: 0x60103810 stack addr: 0x7ffd0f9a436811 stack addr: 0x7ffd0f9a436012 stack addr: 0x7ffd0f9a435813 stack addr: 0x7ffd0f9a435014 read only string addr: 0x40080015 argv[0]: 0x7ffd0f9a481116 env[0]: 0x7ffd0f9a481917 env[1]: 0x7ffd0f9a482e18 env[2]: 0x7ffd0f9a484519 env[3]: 0x7ffd0f9a485020 env[4]: 0x7ffd0f9a486021 env[5]: 0x7ffd0f9a486eenv[6]: 0x7ffd0f9a4892env[7]: 0x7ffd0f9a48a5env[8]: 0x7ffd0f9a48aeenv[9]: 0x7ffd0f9a48f1env[10]: 0x7ffd0f9a4e8denv[11]: 0x7ffd0f9a4ea6env[12]: 0x7ffd0f9a4f00env[13]: 0x7ffd0f9a4f13env[14]: 0x7ffd0f9a4f24env[15]: 0x7ffd0f9a4f3benv[16]: 0x7ffd0f9a4f43env[17]: 0x7ffd0f9a4f52env[18]: 0x7ffd0f9a4f5eenv[19]: 0x7ffd0f9a4f93env[20]: 0x7ffd0f9a4fb6env[21]: 0x7ffd0f9a4fd5env[22]: 0x7ffd0f9a4fdf
6.2虚拟地址
来段代码感受⼀下
#include #include #include int g_val = 0;int main(){pid_t id = fork();if(id < 0){perror(\"fork\");return 0;}else if(id == 0){ //childprintf(\"child[%d]: %d : %p\\n\", getpid(), g_val, &g_val);}else{ //parentprintf(\"parent[%d]: %d : %p\\n\", getpid(), g_val, &g_val);}sleep(1);return 0;}
结果如下:
我们发现,输出出来的变量值和地址是⼀模⼀样的,很好理解呀,因为⼦进程按照⽗进程为模版,⽗⼦并没有对变量进⾏进⾏任何修改。可是将代码稍加改动:
#include #include #include int g_val = 0;int main(){pid_t id = fork();if(id < 0){perror(\"fork\");return 0;}else if(id == 0){ //child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程再读取g_val=100;printf(\"child[%d]: %d : %p\\n\", getpid(), g_val, &g_val);}else{ //parentsleep(3);printf(\"parent[%d]: %d : %p\\n\", getpid(), g_val, &g_val);}sleep(1);return 0;}
结果:
我们发现,⽗⼦进程,输出地址是⼀致的,但是变量内容不⼀样!能得出如下结论:• 变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量 • 但地址值是⼀样的,说明,该地址绝对不是物理地址! • 在Linux地址下,这种地址叫做 虚拟地址 • 我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,⽤⼾⼀概看不到,由OS统⼀ 管理。OS必须负责将 虚拟地址 转化成 物理地址 。
6.3进程地址空间
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图: