赞
踩
本文主要介绍进程的相关知识
几乎所有的计算机都是遵守冯诺依曼体系的,计算机是由一个个硬件组成的,配套着操作系统就可以发挥出其功能。
冯诺依曼体系将计算机的硬件分成了主要的三大块
为什么要有内存这个东西呢?
从技术角度分析
cpu的运算速度 > 寄存器的速度 > 缓存的速度 > 内存的速度 > 外设的速度(磁盘等)
因为数据大多都是需要cpu计算的 但是输入输出设备的速度和cpu的速度完全不是一个级别的,cpu速度远远快于外设速度,那么需要计算的数据直接交给cpu处理的话就会因为外设的速度慢而使得cpu的效率发挥不出来,所以cpu都是不和外设直接打交道的,都是通过内存来交流的,因为内存的速度相比外设快,外设可以将数据加载到内存,然后让cpu找内存拿数据,处理完后返回给内存,再回到外设,这样设计就可以达到视频外设和cpu速度不均的问题,提高效率。
看到这里我们可能会有一个疑惑,cpu和外设速度相差大就让外设加载数据到内存,那么外设加载的速度还是很慢啊,加载到内存也还是会很慢啊,那cpu还是会需要等待内存给它提供来自外设的数据,效率不还是没有提升吗?
这里举例说明吧,我们计算机是有预装载的机制的,就像我们开机的时候,需要等上个几十秒,这就是计算机在把操作系统加载到内存,有了操作系统计算机才可以运行起来,这个等待的过程就是预装载,刚开机会觉得电脑有些卡,但是过了一段时间就很顺畅,这就是因为计算机在加载数据到内存里;还有就是我们在写一个程序的时候会编译运行,运行之前这个程序一定是要被加载到内存的,但是并不是我们什么时候运行就什么时候加载,而是计算机会提前把我们写的代码加载到内存中,这就是预装载,可以是的数据提前被加载到内存,提高效率。(预装载是体系结构规定的)
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
操作系统生来就是为了管理软硬件资源的,是一个搞管理的软件
思考:c/c++代码可以在windows和linux不同的系统下跑起来的,但是不同的系统的系统接口(系统调用)是不同的,那在两个系统上跑的都是一样的代码(c、c++代码),代码的执行结果为什么是一样的呢?
这就要说到c/c++的库了,库会帮我们分辨不同的平台,进而使得我在linux下就是调用的linux的系统调用,在Windows下就是调用的windows的系统调用,是类似多态的原理的,调用printf语句,在linux下就会调linux的对应的系统调用将数据打印到显示器,反之亦然。同一个对象执行相同动作,表现出不同的结果,这就是多态。
进程的概念有两个层面上的解释:
教科书上的解释:进程是程序的一个执行实例,是正在执行的程序。
内核上的观点:担当分配系统资源(cpu 时间,内存)的实体。
进程的信息会被放到一个叫进程控制块的数据结构中,可以理解成它是进程属性的集合。linux中的PCB实际上是一个叫做task_struct的结构体
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
task_struct中的内容
//截取源码部分 struct task_struct { volatile long state;//状态 /* -1 unrunnable, 0 runnable, >0 stopped */ void *stack;//栈 atomic_t usage; unsigned int flags; /* per process flags, defined below */ unsigned int ptrace; int lock_depth; /* BKL lock depth */ /* task state */ int exit_state; int exit_code, exit_signal; int pdeath_signal; /* The signal sent when the parent dies */ /* ??? */ unsigned int personality; unsigned did_exec:1; unsigned in_execve:1; /* Tell the LSMs that the process is doing an * execve */ unsigned in_iowait:1; /* Revert to default priority/policy when forking */ unsigned sched_reset_on_fork:1; pid_t pid;//标识符 pid_t tgid; //... };
六字真言:“先描述,再组织”
操作系统上跑的是一个个程序,也就是进程,那么操作系统就是要管理这些进程,怎么管理呢?
进程有它的对应的属性(下面会有介绍),因为linux操作系统是用c语言写的,所以进程的属性是用结构体来描述的(先描述),这些结构体会被一个双链表链接起来,所以对进程的管理也就演变成了对数据结构的管理。
将进程用一个数据结构描述,linux中用task_struct 结构体来描述,将进程的所有属性都包含在这个结构体中(里面会有指针指向进程的代码和数据)。一个进程的组成就是其task_struct 结构体和其对应的代码和数据。
将一个个进程的数据结构组织在一起,linux中使用双链表将这些结构体一个个连起来,完成对进程数据结构的组织。
管理进程就是变成了对数据结构的管理,管理好这些数据结构就可以管理好进程!!!
进程在内存中的样子:
如何将进程运行起来:
介绍几种方式:
一、ps命令查看
ps axj | grep 文件名 | grep -v grep
将proc程序跑起来,形成一个进程
用上面的命令进行查看进程!
二、通过/proc系统文件夹进行查看
这里的查看某个进程是需要明确该进程的pid才可以进行查看的,因为在/proc文件夹中大多都是存的进程的pid,通过pid来标识进程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pHAqbk2Z-1677761095396)(C:\Users\华哥\AppData\Roaming\Typora\typora-user-images\image-20221009002430289.png)]
三、通过top命令查看
man 3 getpid
getpid()是获取进程本身的标识符 pid
getppid()是获取当前进程的父进程的 pid
代码演示:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
printf("我是一个进程!\n,我的pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
注意的点:
为什么对父进程就是返回子进程的pid,对创建出的子进程就是返回的0呢?
因为一个父进程可以拥有多个子进程,所以需要父进程需要去标识每个子进程,那么就需要知道子进程的pid,故调用fork()函数创建子进程,fork()会给父进程返回创建出来的子进程的pid;对于子进程,它只有一个父进程,并且一个进程被创建出来就是知道自己和其父亲的pid的,所以fork()对创建出来的进程的返回值为0即可;当创建失败就返回-1;
#include<stdio.h> #include<unistd.h> #include<sys/types.h> int main() { pid_t id=fork(); while(1) { if(id>0) { printf("我是父进程! 我的pid:%d ppid:%d \n",getpid(),getppid()); } else if(id==0) { printf("我是子进程! 我的pid:%d ppid:%d \n",getpid(),getppid()); } else { printf("创建失败!\n"); } sleep(1);//循环一次休眠一秒 } return 0; }
可以看出运行的结果是交替打印父子进程的信息 因为这里调用fork()创建出了子进程,那么就会有两个进程,从而会使得程序有两个执行流,父子进程都会执行各自的执行流,所以在循环的作用下就会出现交替打印父子进程信息的结果,这也是父子进程执行各自执行流的结果。
父子进程代码是共享的,数据会各自开辟空间 私有一份(会有写时拷贝,也就是当子进程要改变父进程中的数据就会独立开辟一块空间,建立自己的数据,使得自己不会影响到父进程的数据,实现进程的独立性)
fork()一般都会用if else 语句实行分流操作 使得子进程去完成具体的某项工作 而不是和父进程执行一样的操作
思考:上面的代码中的pid_t id =fork() 为什么一个id可以接收不同的返回值呢?
进程运行就是进程在运行队列中准备就绪,等待cpu执行其代码的状态;进程终止就是进程通过return或exit或遇到异常退出,随时等待操作系统回收其资源的状态。
什么是进程阻塞?
一个进程不仅仅会申请cpu的资源,还可能会申请其他设备的资源,如磁盘网卡显示器等,当一个进程在等待队列中准备就绪,当cup资源就绪时,开始执行该进程的代码,如果该进程的代码涉及到申请其他资源的时候(例如访问磁盘资源,涉及文件相关操作),如果此时的某个设备资源正在别其他进程使用,使得该进程申请资源时资源不就绪,那么该进程的task_struct 就会被操作系统从运行队列中移除,然后放到操作系统中描述对应设备资源的数据结构中(一般是一个等待队列里),我们称进程等待某个资源就绪的状态为阻塞状态。
什么是进程挂起?
当内存中的进程太多(其数据和代码都会一并加载到内存中),就会使得内存的空间不足,那么操作系统就会将短期内不会被调度的进程的代码和数据置换回磁盘中,以此来缓解内存空间不足。代码和数据被置换回磁盘的进程就会被操作系统挂起!!!(此操作一般会涉及磁盘的高频访问)
为了表示进程运行的情况,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 */
};
演示:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
//啥也不干 就一直死循环 就会一直在运行队列里 就是R状态
}
return 0;
}
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("i am a process\n");
sleep(1);
}
return 0;
}
窗口1:
窗口2:
监视窗口:
上面的操作涉及进程信号知识,后面的博客会讲解,这里只需知道19 号信号可以使进程暂停即可!
演示在下面!
#include<stdio.h> #include<unistd.h> #include<sys/wait.h> #include<stdlib.h> int main() { pid_t id= fork(); if(id>0) { //parent printf("i am parent\n"); sleep(15);//休眠15s但是不退出 exit(0); } else if(id==0) { //child int n=10;//10s后退出 如果父进程未读取其退出代码 该进程会变成僵尸进程 成Z状态 while(n--) { printf("i am child\n"); sleep(1); } printf("子进程退出!\n"); exit(0); } else { printf("creat fail!\n"); } return 0; }
程序运行:
脚本监控进程状态:
//shell脚本
while :;do ps axj | head -1 && ps axj | grep myproc | grep -v grep;sleep 1;echo "#######################################################" ;done
什么是孤儿进程?
顾名思义,就是其父亲不要它了,这个进程成了孤儿。也就是一个进程的父进程提前退出,但是其子进程还没有退出,那么该子进程就会变成失去父亲的孤儿进程(孤儿进程会被1号进程收养,回收资源)
#include<stdio.h> #include<sys/types.h> #include<unistd.h> #include<stdlib.h> int main() { pid_t id= fork(); if(id==0) { //child int n=10; while(n--) { sleep(1); printf("i am child! pid:%d i am running!\n",getpid()); } } else if(id>0) { int m=5; //parent while(m--) { printf("i am parent! pid:%d i am running!\n",getpid()); sleep(1); } exit(0); } else{ //error printf("fork fail!\n"); } return 0; }
监控:
因为计算的各种资源是有限的,但是进程却很多,那么进程之间就会有竞争,类比现实生活中的排队现象,我们都是站成一条长队,排在前面的就优先,同样的对应进程间也有需要进行排队使用资源,于是就有了有进程优先级。
使用top命令就可以看到
上图中的PR就是进程优先级。
NI是进程优先级的修正数值,可以通过改变进程的NI值对进程优先级进行适度的调节!
优先级公式 : P R θ ( n e w ) = P R θ ( o l d ) + N I ( N I ϵ [ − 20 , 19 ] ) 优先级公式:PR_\theta(new)=PR_\theta(old)+NI \space\space\space (NI\epsilon[-20,19]) 优先级公式:PRθ(new)=PRθ(old)+NI (NIϵ[−20,19])
更据上面的公式可以知道,进程优先级的调节是通过修改对应进程的NI值来实现的。
//步骤
1、输入top命令
2、再依次输入 r + 修改的进程pid + 回车 + 对应的nice值
前后对比:
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
//执行下面指令即可
1.查看所有环境变量 env命令会将所有的环境变量都显示出来
env
2.查看具体名称的环境变量
echo $环境变量名
我们平常看到的main函数大多是不带参数的,但实际上它是有参数的,main函数可以带三个参数!!!
//第一个参数是执行main函数对应的进程的指令的个数,第二个是执行其对应进程的指令字符串,第三个是其对应进程的环境变量(字符指针数组,里面存的是一个个的环境变量字符串)
int main(int argc,char* argv[],char* env[])
{
printf("%d\n",argc);
for(int i=0;i<argc;i++)
printf("%s\n",argv[i]);
for(int i=0;i<argc;i++)
printf("%s\n",env[i]);
return 0;
}
#include <stdio.h>
int main(int argc, char *argv[])
{
//可以通过environ找到存储环境变量的指针数组,进而打印里面的环境变量(一个个字符串)
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
char getenv(const char name)**
参数name为所需要获取的环境变量的名称,如果该环境变量在环境表中存在那么就会返回所要获取的环境变量(字符串),反之如果不存在就会返回空指针!
#include<stdio.h>
#include<stdlib.h
int main(int argc,char* argv[],char* env[])
{
extern char** environ;
int i;
printf("putenv之前环境变量表:\n");
for(i=0;environ[i];i++)
printf("%s\n",environ[i]);
putenv("MYPATH = 66666666666666666666666");
printf("putenv之后环境变量表:\n")
for(i=0;environ[i];i++)
printf("%s\n",environ[i]);
return 0;
}
注:由于putenv会存在安全问题(野指针),可以使用setenv()来代替putenv() !
在命令行中我们可以定义两种变量,一种是本地变量,一种是环境变量。
本地变量不具有全局属性,不会被子进程继承!!!
因为我们自己写的程序运行起来形成的进程,都是有bash创建的子进程,那么我们在命令行中定义本地变量,再到我们自己写的程序中去找在bash中定义的本地变量如果找得到说明本地变量是可以被子进程继承的,否则不可以!!!
注意:这里要清楚的理解我们写的代码形成的进程都是由bash创建的子进程!!!
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("%s\n",getenv("LocalVal"));
return 0;
}
我们只需要使用关键字export就可以将一个本地变量导出为环境变量!!!
export LocalVal=1234567//那么LocalVal就会被导进bash的环境表中了 注:=两边不要加空格!!!
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("%s\n",getenv("MyVal"));
return 0;
}
我们在学语言的时候经常听到什么全局变量是存在静态区,常量字符串是存在常量区,定义的普通变量是存在栈上的,malloc出来的数据是存在堆上的,这里的堆,栈,常量区,静态区都是程序地址空间的叫法!
但是一个程序运行起来就是一个进程,那么原来的程序地址空间的叫法是不准确的,应该叫做进程地址空间。
什么是进程地址空间呢?
所谓的进程地址空间是操作系统通过软件的方式,给进程提供一个软件视角(进程地址空间),认为自己是独占着整个系统的所有资源(内存),每个进程运行起来的时候,操作系统都会先给其创建一个进程地址空间(mm_struct),将进程所需要的空间先规划好,当其真正需要使用的时候再给它分配,这样就避免了进程一运行操作系统就需要立马给其分配内存,提高了内存的使用效率。
如何管理地址空间呢?
还是六字真言:“先描述,再组织”
既然进程地址空间是一个个的内核数据结构mm_struct 那么管理好mm_struct 就可以管理好进程地址空间!
这里是大概的简略的组成(其真实组成远不止这么点东西,这里只介绍其区域划分),其实源码是有一个vm_area_struct 结构体来完成各个区域的划分的(了解即可,无需深究!)
//笼统理解为下面结构 struct mm_struct { //各个区域的划分 unsigned int code_start; unsigned int code_end; unsigned int stack_start; unsigned int stack_end; unsigned int init_data_start; unsigned int init_data_end; unsigned int uninit_data_start; unsigned int uninit_data_end; ... }
注意:这里的进程地址空间并不是直接对应的物理地址,而是虚拟地址,物理地址和虚拟地址是通过页表转换的!
出于安全考虑,进程虽然认为只有其自己独占整个内存,但是这是操作系统为其画的大饼,实则是多个进程在共用一块内存,那么就会有多个进程会用到同一块物理空间的可能,如果每个进程都是直接访问的物理内存就会容易出现野指针的问题,例如原本多个进程共用的空间,其中一个进程终止了对该空间释放,那么就会使得原来与其共用一块内存的进程访问该空间时出现野指针,所以直接访问物理地址是具有安全隐患的!!!
通过添加一层软件层,完成有效的对进程操作内存的风险管理(权限管理),本质是为了保护物理内存各个进程的数据安全
将内存申请和内存使用在时间上解耦。通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存操作和OS进行内存管理进行软件层面上的分离。
感兴趣的可以去写代码验证上面的排列。
#include<stdio.h> #include<sys/types.h> #include<unistd.h> int a=10; int main() { pid_t id=fork(); if(id>0) { //parent int n=10; while(n--) { printf("我是父进程 pid:%d ppid:%d a:%d &a:%p\n",getpid(),getppid(),a,&a); sleep(1); } } else if(id==0) { //child int n=10; while(n--) { if(n==5) { printf("我是子进程 我修改了a 为 200!\n"); a=200; } printf("我是子进程 pid:%d ppid:%d a:%d &a:%p\n",getpid(),getppid(),a,&a); sleep(1); } } else { perror("fork error!"); } return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。