重庆高端网站建设,wordpress discuz建站,wordpress优秀网站,中山网站建设文化咨询目录进程概念进程控制块-PCBPCB的内容分类标识符查看进程信息的方法状态fork函数进程状态R运行状态#xff08;running#xff09;S睡眠状态#xff08;sleeping#xff09;D磁盘休眠状态#xff08;Disk sleep#xff09;T停止状态#xff08;stopped#xff09;X死亡…
目录进程概念进程控制块-PCBPCB的内容分类标识符查看进程信息的方法状态fork函数进程状态R运行状态runningS睡眠状态sleepingD磁盘休眠状态Disk sleepT停止状态stoppedX死亡状态dead优先级进程优先级基本概念PRI和NItop-查看进程优先级的命令程序计数器内存指针上下文数据I/O状态信息记账信息特殊进程Z(zombie)-僵尸进程僵尸进程的作用僵尸进程的危害孤儿进程关于进程的其他概念并行和并发的区别程序地址空间实验环境c语言角度的内存空间进程地址空间mm_structmm_struct的理解页表页表的作用写时拷贝概念解释代码现象为什么需要写时拷贝总结进程概念 进程Process是计算机中的程序关于某数据集合上的一次运行活动是系统进行资源分配和调度的基本单位是操作系统结构的基础。 进程控制块-PCB 进程信息都是放在一个叫进程控制块即PCBprocess control block可以简单地理解为进程数据集合。 在操作系统的学习中我们知道了操作系统要管理进程就要做到先描述再使用在Linux中内核描述进程的结构体叫做task_struct。
PCB的内容分类
标识符 标识符描述进程的唯一标识符用来区别其他进程。 首先我们来认识一个命令叫做ps-显示进程状态 语法ps [参数] 常用参数参考详情 我们可以看到有PPID和PIDPID是本进程的标识符PPID是父进程的标识符。
查看进程信息的方法
上面的ps命令通过/proc系统文件夹来查看 通过系统调用来获取进程标识符getpid()和getppid()
#include stdio.h
#include sys/types.h
#include unistd.h
int main()
{printf(pid: %d\n, getpid());printf(ppid: %d\n, getppid());return 0;
}状态 状态任务状态、退出代码退出信号等等 fork函数
首先我们认识一下系统调用接口fork-创建一个进程 #includestdio.h#includeunistd.h#includesys/types.hint main(){pid_t idfork();if(id 0){//fatherprintf(pid:%d,ppid:%d\n,getpid(),getppid());sleep(5);}else if(id 0){//childprintf(pid:%d,ppid:%d\n,getpid(),getppid());sleep(5);}else{perror(fork);return 1;}return 0;} 在fork创建一个新进程后子进程执行和父进程相同的代码我们往往通过它的返回值进行分流使父子进程执行不同的代码。
进程状态
首先我们看看Linux的源代码 我们可以通过源代码看出task_struct中通过一个字符指针数组来进行进程状态分类。 我着重介绍R、S、D、t、T、Z这几个状态。
R运行状态running R运行状态表示进程在运行中或者在运行队列中可被cpu调度不一定正在运作。 我们写一个死循环看看。
#includestdio.h
int main()
{while(1){}return 0;
}一定要记住进程是R状态不一定在运行
S睡眠状态sleeping S睡眠状态表示进程正在休眠不会被cpu调度直到被唤醒此状态可以被杀掉。 #includestdio.h
#includeunistd.h
int main()
{sleep(10);return 0;
}此状态的意义是在进程需要某种资源才能继续跑下去时我们将进程进行睡眠节约cpu资源在条件满足时将进程唤醒。
D磁盘休眠状态Disk sleep D磁盘休眠状态也叫不可中断睡眠状态相比较S状态在D状态时不能被杀掉OS也不行。 此状态不好实现我们就讲讲为什么会有一个和S状态如此相像的D状态。 在一个进程向硬盘写入数据时进程应该进入睡眠模式等待硬盘回应如果此时内存空间严重不足时OS可能会干掉一些S状态的进程分配给活跃的进程此时就导致了一个问题那就是如果硬盘在写入数据出现错误时它向进程询问该怎么办但是此时进程已经被OS干掉了就会陷入一个尴尬的场景。 那么上面描述中是谁的错 答案是都没错 进程我与硬盘进行IO时进入睡眠模式节约cpu资源结果被cpu干掉了我能怎么办 硬盘我在存入数据本来就有几率出现错误但是我回去问进程该怎么办它一直不回答我能怎么办 OS内存资源已经非常少了我干掉一些偷懒的进程把资源分配给活跃的进程我有什么错。 所以为了避免上面这种情况我们需要一个在睡眠状态不能被干掉的状态那就是D状态。
T停止状态stopped T停止状态可以通过发送SIGSTOP信号让进程停止可以通过发送SIGCONT信号让进程继续运行。 这里借用R状态的代码 首先进程处于R状态kill命令可以给指定进程发信号在我们向进程发送SIGSTOP时我们可以看到进程的STAT从R变成了T在发送SIGCONT信号后STAT又变成了R。
信号有关的命令以后细嗦先放个图了解一下。
X死亡状态dead X死亡状态这个状态只是一个返回状态我们不能从ps中的STAT列表中看到此状态。 优先级 优先级代表进程被cpu调度的优先级。 进程优先级
基本概念
cpu分配资源给进程的先后顺序就是进程的优先级进程的优先级高优先被cpu调度。将不重要的进程优先级降低可以提高cpu资源利用。
我们来看看ps -l中的信息 其中与进程优先级有关的数据就是PRI和NI
PRI和NI PRI代表进程的优先级越低优先级越高。 NI代表进程的nice值 PRI很好理解那NI所代表的nice值是什么呢
nice值我们可以简单地理解为PRI的修正数据PRI越低优先级越高那引入nice值后(new)PRI(old)PRINInice值的范围为-20~19。我们通过修改NI调整一个进程的优先级。
top-查看进程优先级的命令
top有点类似Windows下的任务管理器 进入top输入“r”-输入修改进程的pid-输入nice值不管是降低还是增加nice值都需要sudo提高权限
修改前 输入“r” 输入nice值 修改成功 我们可以看出**(new)PRI(old)PRINI**
程序计数器 程序计数器程序中即将被执行的下一行代码的地址 这就是为什么程序知道自己的该怎样执行代码。
内存指针 内存指针包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针。 进程可以通过内存指针找到自己的代码和数据以及共享内存块。
上下文数据 上下文数据进程执行时寄存器中的数据 我们如何来理解这个上下文数据呢 首先我们要明白cpu运行进程1执行到i时,cpu需要将i取到寄存器中进行自加一然后再写入到内存中Linux中进程是通过时间片算法切换运行达到一个并发的效果但是如果cpu刚将i完成准备从寄存器中把i写入到内存时进程1的时间片到了cpu就会切换下一个进程而cpu的寄存器是有限的所以进程1在寄存器中的数据就会被覆盖如果下次轮到进程1的时间片时cpu又会重复上述过程导致cpu资源浪费所以为了避免这种情况我们需要将cpu寄存器中的数据在切片前保存到上下文数据中这样的话下次cpu跑该进程时先把上下文数据读取到寄存器中再执行下一步命令。
I/O状态信息 IO状态信息: 包括显示的I/O请求,分配给进程的IO设备和被进程使用的文件列表。 记账信息 记账信息: 可能包括处理器时间总和使用的时钟数总和时间限制记账号等。 特殊进程
Z(zombie)-僵尸进程
僵尸进程是一种特殊的状态。当进程退出时并且父进程没有读取进程的退出信息时就会一直保持Z僵尸模式直到父进程读取。僵尸进程的生命周期为进程退出父进程存在并读取前。
不多bb上代码
#includestdio.h
#includeunistd.h
#includesys/types.h
int main()
{pid_t idfork();if(id 0){//childprintf(i am child,pid:%d,ppid:%d\n,getpid(),getppid());sleep(5);}else if(id 0){//fatherprintf(i am father,pid:%d,ppid:%d\n,getpid(),getppid());sleep(30);}else {perror(fork);return 1;}return 0;
}僵尸进程的作用
帮助父进程了解子进程的任务完成情况以及退出信息。比如一个人突然死了有人发现后选择报警肯定不会乱动尸体除了凶手警察发现尸体后也会保留案发现场这些都是因为尸体和现场会留下信息让警察去侦察这个人是怎么死的。
僵尸进程的危害 进程的退出信息是保存在PCB中如果父进程一直不读取子进程退出信息这个PCB就会一直存在浪费内存资源。 孤儿进程
在子进程退出父进程未读取时会产生僵尸进程那么如果父进程比子进程先退出那子进程的退出信息又改由谁来回收呢父进程先退出子进程就会变成孤儿进程然后被1号进程领养从而1号进程负责回收子进程的退出信息。
这个代码与僵尸进程的代码相似。
#includestdio.h
#includeunistd.h
#includesys/types.h
int main()
{pid_t idfork();if(id 0){//childprintf(i am child,pid:%d,ppid:%d\n,getpid(),getppid());sleep(30);}else if(id 0){//fatherprintf(i am father,pid:%d,ppid:%d\n,getpid(),getppid());sleep(5);}else {perror(fork);return 1;}return 0;
}关于进程的其他概念
竞争性系统中进程数目众多cpu资源只有少量所以进程会竞争cpu资源的使用权操作系统为了使进程合理使用cpu资源便有了优先级。独立性多进程运行会独享很多资源各个进程运行中互不干扰。并行多个进程在多个cpu下同时运行。并发多个进程在单个cpu下采用进程切换的方式在一个时间段中使多个进程得到推进。
并行和并发的区别 并行是每个进程都有自己的cpu在每一刻多个进程同时运行而并发是很多进程轮流使用一个cpu资源例如Linux采用时间片切换的算法使多个进程在一个时间段中看起来像是同时进行系统下进程数目往往远大于cpu资源所以并发在大多数场景下更为合理。 程序地址空间
实验环境 kernel 2.6.32 32位平台 c语言角度的内存空间
相信大家在学习c语言的时候一定听过什么内存分为栈区堆区等等栈区向下生长、堆区向上生长也看过下面这一副图。 我们可以通过一段代码来验证一下是不是如此。
#includestdio.h
#includestdlib.h
static int init_value10; //static修饰的全局变量只能在本文件中访问
static int uninit_value;int main(int argc)
{int i0;int* pimalloc(sizeof(int));printf(%p\n,argc);printf(%p\n,i);printf(%p\n,pi);printf(%p\n,uninit_value);printf(%p\n,init_value);printf(%p\n,main);return 0;
}我们可以看出确实如此接下来我们看看另一段代码 #includestdio.h#includeunistd.h#includesys/types.hint global_val10;int main(){pid_t idfork();if(id 0){//fatherprintf(i am father,%p,%p\n,global_val,main);}else if(id 0){//childprintf(i am child,%p,%p\n,global_val,main);}else{perror(fork);return 1;}return 0;} 通过运行结果我们可以得到一个结论那就是在父子进程未对数据进行修改时子进程的代码和数据是和父进程共享的。
接下来我们在上述代码的基础上进行一些小小的改变。 #includestdio.h#includeunistd.h#includesys/types.hint global_val10;int main(){pid_t idfork();if(id 0){//fathersleep(5)printf(i am father,%p,%d\n,global_val,global_val);}else if(id 0){//childglobal_val20;printf(i am child,%p,%d\n,global_val,global_val);}else{perror(fork);return 1;}return 0;}
注意这里让父进程休眠了5秒子进程将global_val的值改成了20见证奇迹的时候到了。
我们可以看到父子进程的val不一样但是他们的地址却是一样的这到底是为什么呢
首先他们的val不一样说明父子进程输出的val绝对不是同一个变量。其次他们的地址一样说明这个打印出来的地址并不是真正的物理地址。
那这个地址到底是什么东西呢
在Linux下这个地址叫做虚拟地址听名字我们就知道了它不是真正的物理地址。注意我们通过C/C打印看到的地址统统都是虚拟地址用户看不到物理地址物理地址由操作系统管理。
进程地址空间
有人看到这个标题肯定会有疑问你怎么一会一个程序地址空间一会一个进程地址空间呢其实严格上讲这个应该叫做进程地址空间。
那进程地址空间到底是什么样子的呢其实它也是一个结构体话不多说上源码。
mm_struct
struct mm_rss_stat {atomic_long_t count[NR_MM_COUNTERS];
};
/* per-thread cached information, */
struct task_rss_stat {int events; /* for synchronization threshold */int count[NR_MM_COUNTERS];
};
#else /* !USE_SPLIT_PTLOCKS */
struct mm_rss_stat {unsigned long count[NR_MM_COUNTERS];
};
#endif /* !USE_SPLIT_PTLOCKS */struct mm_struct {struct vm_area_struct * mmap; /* list of VMAs */struct rb_root mm_rb;struct vm_area_struct * mmap_cache; /* last find_vma result */
#ifdef CONFIG_MMUunsigned long (*get_unmapped_area) (struct file *filp,unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags);void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endifunsigned long mmap_base; /* base of mmap area */unsigned long task_size; /* size of task vm space */unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */pgd_t * pgd;atomic_t mm_users; /* How many users with user space? */atomic_t mm_count; /* How many references to struct mm_struct (users count as 1) */int map_count; /* number of VMAs */spinlock_t page_table_lock; /* Protects page tables and some counters */struct rw_semaphore mmap_sem;struct list_head mmlist; /* List of maybe swapped mms. These are globally strung* together off init_mm.mmlist, and are protected* by mmlist_lock*/unsigned long hiwater_rss; /* High-watermark of RSS usage */unsigned long hiwater_vm; /* High-water virtual memory usage */unsigned long total_vm, locked_vm, shared_vm, exec_vm;unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack;unsigned long arg_start, arg_end, env_start, env_end;unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv *//** Special counters, in some configurations protected by the* page_table_lock, in other configurations by being atomic.*/struct mm_rss_stat rss_stat;struct linux_binfmt *binfmt;cpumask_t cpu_vm_mask;/* Architecture-specific MM context */mm_context_t context;/* Swap token stuff *//** Last value of global fault stamp as seen by this process.* In other words, this value gives an indication of how long* it has been since this task got the token.* Look at mm/thrash.c*/unsigned int faultstamp;unsigned int token_priority;unsigned int last_interval;/* How many tasks sharing this mm are OOM_DISABLE */atomic_t oom_disable_count;unsigned long flags; /* Must use atomic bitops to access the bits */struct core_state *core_state; /* coredumping support */我们之前提到操作系统要进行管理就要**“先描述再组织”**内存管理也不例外mm_struct就是操作系统用来进行内存管理的结构体。
那么结构体里面的变量都是什么意思为什么操作系统可以通过这些变量来进行内存管理呢 首先我们要认识一个问题那就是我们是如何管理地球上的土地内存的各个国家通过划分边界线的方式来确定自己可以使用的土地而这些国家通过气候环境等等因素将自己的土地划分成住宅区、农耕区、工业区我们往往通过尺度单位记录起始位置结束位置来划分土地而只要边界确定好了我们就可以通过边界来确认自己使用土地是否合法。 mm_struct的理解
对应到操作系统来说划分区域是为了更好地管理内存提高管理内存效率而mm_struct中的一个个unsigned long变量就是每个区域的起始位置和结束位置操作系统可以采用基地址偏移量的方法来判断内存访问和申请是否合法。
页表
我们都知道mm_struct中的地址都是虚拟地址不是真正的物理地址那么操作系统通过mm_struct来管理真正的物理内存呢 接下来我们要引出一个概念叫做页表。 页表是一种特殊的数据结构放在系统空间的页表区存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表PCB表中有指针指向页表。 页表的作用
存储虚拟地址和物理地址的映射进程可以通过页表将虚拟地址找到物理地址并进行数据的增删查改。不会将真正的物理地址暴露给用户保护内存。
写时拷贝
概念 写时拷贝是一种可以推迟甚至免除拷贝数据的技术。 内核此时并不复制整个进程地址空间而是让父进程和子进程共享同一个拷贝。 只有在需要写入的时候数据才会被复制从而使各个进程拥有各自的拷贝。 解释代码现象
现在可以来解释这个神奇的结果了。
我们通过图来说明上面的代码问题。
在父子进程fork子进程时子进程复制父进程的数据形成task_struct和mm_struct以及页表此时父子进程的global_val指向同一个物理地址。子进程拥有自己的task_struct和mm_struct以及页表但是内容与父进程相同当子进程执行到global_val20时就会发生写时拷贝子进程会在物理内存上申请一个新的空间用来存放自己的global_val变量接着子进程只需把页表中对应的物理内存修改成新的物理内存地址不需要修改虚拟内存地址所以我们打印出来的地址一样。
为什么需要写时拷贝 因为进程之间具有独立性就算是父子进程也不能影响其他进程的数据并且在创建子进程时不开辟新的物理内存等到需要时在申请空间可以高效地使用内存空间。 总结
子进程的创建实质就是task_struct和mm_struct以及页表的创建拷贝父进程的数据。进程地址空间mm_struct和页表避免了系统级别的越界问题即错误访问物理内存。每个进程对待内存的看法都一样因为mm_struct一样。
写在最后文中Linux源码来自Linux源码创作不易如果这篇文章对你有帮助希望大佬们三连支持。