前言:
在学习操作系统的过程中,我们常常能听到进程这一概念以及相关的一些知识。例如什么是父进程,什么是子进程,如何创建子进程,如何杀死进程等。这些知识后面会一一介绍,在迈入学习进程的第一步我只需要知道在winds系统下任务管理器中的应用就是进程,而它们都是可执行文件即,所以换句话话说:进程其实就是一个可执行程序的实例
那么进程在系统中到底扮演的是什么角色呢?
进程在系统中的作用是非常重要的,程序的运行需要系统资源的支持,而操作系统中各种各样的程序运行,其资源的合理分配和各种的程序运行,其资源都是合理的分配的各种程序之间的协调都是进程的工作。
在进程概念这一节,会从冯诺依曼体系结构开始说起--为了构建一个整体框架,便于后续的理解,然后我们在深入理解进程概念-PCB,创建进程,掌握僵尸进程和孤儿进程,及其形成原因和危害,了解进程调度,Linux进程优先级,理解进程竞争性与独立性,理解并行与并发。理解环境变量,熟悉常见环境变量及相关指令, 进程地址空间等等。当我们学习这些后,我们会发现进程是对正在运行程序的一个抽象,操作系统其他的所有内容都是围绕着进程的概念展开的。
目录
前言:
冯诺依曼体系结构
操作系统(Operator System)
操作系统的概念
如何理解管理
描述与组织
理解管理软硬件资源
系统调用
计算机的软硬件体系结构图
编辑总结
进程
操作系统对进程的管理
先描述
再组织
task_ struct内容分类
Linux中的-PCB
查看进程
查看进程
见见系统调用
通过系统调用创建进程-fork初识
进程状态
宏观概念
挂起状态
Linux操作系统的的状态
进程状态查看
两个特殊的进程
Z(zombie)-僵尸进程
僵尸进程危害
孤儿进程
进程优先级
基本概念
查看系统进程
用top命令更改已存在进程的nice:
其他概念
进程切换
环境变量
基本概念
常见环境变量
和环境变量相关的命令
通过代码如何获取环境变量
程序地址空间
用段代码感受父子进程地址
感性理解程序地址空间
程序地址空间是什么
程序地址空间--区域划分
为什么会存在进程地址空间
冯诺依曼体系结构
数学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),这套理论被称为冯·诺依曼体系结构。我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
那么关于冯诺依曼体系,有几个问题:
1.这里的存储器指的是什么?
这里的存储器指的是内存,它的特点是掉电已失。
2.输入/输出设备是外设吗?
运算器和控制器的特点是什么?
运算器+控制器+其他=cup,运算器和控制器的计算速度是特别快
CPU的特点是什么?
CPU只能被动的接受别人的指令或数据,然后执行别人的指令,从而到达计算别人数据的目的。
CPU如何认识别人的指令呢?
CPU拥有自己的指令集,分别是精简指令集和复杂指令集。
我们写代码,编译的本质是什么?
CPU通过自己的指令集去识别二进制可执行文件是在做什么。换句话说就是将二进制可执行文件翻译成CPU的指令集,CPU再进行计算。
CPU的数据从哪儿来?
这里就会有一个例子,就是木桶原理:水桶装水的多少不是取决于最长的木板,而是最短的。
那么CPU也是一样的,CPU在读取或写入的时候,在数据层面上来说,CPU读取数据只和较快的内存打交道。
结论:
1.CPU不和外设直接打交道,和内存打交道
2.所有的外设,有数据需要载入,只能载入到内存中,内存写出,也一定是写到外设中
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊 天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发 送文件呢?
操作系统(Operator System)
操作系统的概念
是一个进行软硬件资源进行管理的软件
为什么要进行管理?
通过合理的管理软硬件资源,为用户提供良好的(稳定的,高效的,安全的)执行环境
如何理解管理
在日常生活中,老板管理员工,管理的是员工的数据--员工的业绩,工资,姓名,性别等。如果老板看见员工业绩不好,想辞退该员工,那么只需要将该员工的数据从公司删除即可。
管理的本质:是对数据进行管理
这里老板是管理者,他根据数据做了决策,为了能让这个决策生效,所以就有了执行者(组长)
描述与组织
在做决策前需要描述,当描述完后再对描述的对象进行组织
描述就相当于对员工进行描述,在公司可能会制作一个表格,这里我们将当老板是一个懂计算机的,形成一个结构体,结构体里存着员工信息。形成结构体后,通过所学的数据结构将结构体形成链表,对数据进行增删改查的操作。
描述:
组织:
在计算机中,所有管理的本质逻辑:先描述,再组织
理解管理软硬件资源
比如:老板(软件)管理一个公司,不仅仅是管理员工(软件),还要管理公司里的电脑,桌子,板凳(硬件)。
那么操作系统(软件)来说,操作系统既能管理硬件--磁盘,网卡,显卡....(硬件),又能管理软件--进程管理,文件系统,驱动管理...(软件)
系统调用
为什么会产生系统调用?
操作系统不允许用户直接操作各种硬件资源,因此用户程序只能通过系统调用的方式来请求内核为其服务,间接地使用各种资源。
总结就是:操作系统为了安全,通过操作系统接口进行对软硬件的数据的输入/输出。
计算机的软硬件体系结构图
总结
计算机管理硬件
1. 描述起来,用struct结构体
2. 组织起来,用链表或其他高效的数据结构
系统调用和库函数概念
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分 由操作系统提供的接口,叫做系统调用。 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
进程
上面就说过进程其实就是一个可执行程序的实例,那么程序的本质就是文件,放在磁盘上的
通过冯诺依曼结构体系,我们也大概明白了,可执行程序放在磁盘中,因为磁盘访问太慢,CPU进行数据计算的时候,操作系统会将文件放入内存中,CPU再进行处理。
操作系统对进程的管理
在内存中加载的程序很多,如果不管理,就会出现不知道那个程序先执行,那个程序后执行,如果程序挂掉,或者一直未被执行,那么这个时候是需要操作系统进行管理的。管理--先描述,再组织
先描述
当CPU处理数据前,操作系统会对每个进程的进行管理,形成一个结构体--task_struct,便于操作系统好管理,该结构体就放着进程的各个数据。
struct task_struct {
//进程的所有属性
//该进程对应的代码和属性地址
struct task_struct* next;
}
struct task_struct *pl = malloc(struct task_struct)
pl->..= XXX
p1->addr = 代码和数据的地址
再组织
所谓的对进程进行管理,就变成了进程对应的PCB进行相应的管理,PCB为结构体就就转化成对链表的增删改查。操作系统是通过对struct task_struct进行管理,而struct task_struct中相当于有许许多多的该进程对应的代码和属性地址,它通过 struct task_struct* next进行对每个进程向链接。操作系统只对PCB进行管理而不进行计算,当CPU进行计算的时候,操作系统就会把该进程放再前面让CPU进行处理。CPU就通过获取PCB的struct task_struc中的进程地址空间--一段范围,对内存中的该文件的数据进行识别,然后进行计算,进程地址空间后续会讲。
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。 其他信息
Linux中的-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,被称之为PBC。Linux操作系统下的PCB是:task_struct--进程控制块(结构体)
Linux中task_struct用来控制管理进程,结构如下:
struct task_struct
{
//说明了该进程是否可以执行,还是可中断等信息
volatile long state;
//Flage 是进程号,在调用fork()时给出
unsigned long flags;
//进程上是否有待处理的信号
int sigpending;
//进程地址空间,区分内核进程与普通进程在内存存放的位置不同
mm_segment_t addr_limit; //0-0xBFFFFFFF for user-thead
//0-0xFFFFFFFF for kernel-thread
//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
volatile long need_resched;
//锁深度
int lock_depth;
//进程的基本时间片
long nice;
//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER
unsigned long policy;
//进程内存管理信息
struct mm_struct *mm;
int processor;
//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新
unsigned long cpus_runnable, cpus_allowed;
//指向运行队列的指针
struct list_head run_list;
//进程的睡眠时间
unsigned long sleep_time;
//用于将系统中所有的进程连成一个双向循环链表, 其根是init_task
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_head local_pages; //指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal; //父进程终止是向子进程发送的信号
unsigned long personality;
//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
int did_exec:1;
pid_t pid; //进程标识符,用来代表一个进程
pid_t pgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_t session; //进程的会话标识
pid_t tgid;
int leader; //表示进程是否为会话主管
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; //线程链表
struct task_struct *pidhash_next; //用于将进程链入HASH表
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; //供wait4()使用
struct completion *vfork_done; //供vfork() 使用
unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
//it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value
//设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据
//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。
//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送
//信号SIGPROF,并根据it_prof_incr重置时间.
//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种
//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据
//it_virt_incr重置初值。
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_list real_timer; //指向实时定时器的指针
struct tms times; //记录进程消耗的时间
unsigned long start_time; //进程创建的时间
//记录进程在每个CPU上所消耗的用户态时间和核心态时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
//内存缺页和交换信息:
//min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换
//设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。
//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid
//euid,egid为有效uid,gid
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件
//系统的访问权限时使用他们。
//suid,sgid为备份uid,gid
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; //记录进程在多少个用户组中
gid_t groups[NGROUPS]; //记录进程所在的组
//进程的权能,分别是有效位集合,继承位集合,允许位集合
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
unsigned short used_math; //是否使用FPU
char comm[16]; //进程正在运行的可执行文件名
//文件系统信息
int link_count, total_link_count;
//NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空
struct tty_struct *tty;
unsigned int locks;
//进程间通信信息
struct sem_undo *semundo; //进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
//进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct thread_struct thread;
//文件系统信息
struct fs_struct *fs;
//打开文件信息
struct files_struct *files;
//信号处理函数
spinlock_t sigmask_lock;
struct signal_struct *sig; //信号处理函数
sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位
struct sigpending pending; //进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock;
void *journal_info;
};
进程=内核数据结构(tast_struct)+进程对应的磁盘代码
查看进程
查看进程
myproc.c
#include <stdio.h> #include <unistd.h> int main() { while(1) { printf("我是一个进程!\n"); sleep(1); } return 0; }
Makefile
myporc:myporc.c gcc -o $@ $^ .PHONY:clean clean: rm -f myporc
查看进程脚本
ps ajx | head -1 && ps ajx | grep "myporc"
Process ID(PID)
Linux中标识进程的一个数字,它的值是不确定的,是由系统分配的(但是有一个例外,启动阶段,kernel运行的第一个进程是init,它的PID是1,是所有进程的最原始的父进程),每个进程都有唯一PID,当进程退出运行之后,PID就会回收,可能之后创建的进程会分配这个PIDParent Process ID(PPID)
字面意思,父进程的PIDProcess Group ID(PGID)
PGID就是进程所属的Group的Leader的PID,如果PGID=PID,那么该进程是Group LeaderSession ID(SID)
和PGID非常相似,SID就是进程所属的Session Leader的PID,如果SID==PID,那么该进程是session leaderTPGID:控制终端进程组ID(由控制终端修改,用于指示当前前台进程组)STAT: 进程状态UID:用户标识码TIME:命令常用于测量一个命令的运行时间
进程在调度运行的时候,进程具有动态属性
见见系统调用
man getpid
测试代码:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { while(1) { printf("我是一个进程!,我的ID是:%d\n",getpid()); sleep(1); } return 0; }
每次重新运行程序pid都会改变
因为每次都需要重新加载到内存,就意味着操作系统都会重新创建task_struct,重新分配pid
ls /proc
“/proc/[pid]”目录,pid为进程的数字ID,是个数值,每个运行着的进 程都有这么一个目录。
查看父进程
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { while(1) { printf("我是一个进程!,我的ID是:%d\n,父进程ID是:%d\n",getpid(),getppid()); sleep(1); } return 0; }
命令行上启动的进程,一般它的父进程没有特殊情况的话,都是bash
通过系统调用创建进程-fork初识
运行 man fork 认识fork
fork有两个返回值
父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
创建进程
#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.一个CPU一个运行队列
2.让进程入队列的本质是:将该进程的struct stsk_struct结构体对象放入运行队列中
3.进程PCB在runqueue,就是R--运行状态,而不是这个进程正在运行才叫运行状态
4.进程不只会等待(占用)CPU资源,也可能随时随地要外设资源
5.所谓的进程不同的状态,本质是进程在不同的队列中,等待某种资源
挂起状态
阻塞和挂起的区别
挂起了一定阻塞,阻塞了不一定挂起。阻塞时该进程的代码和数据都在内存中,当内存不足时,就会产生挂起状态该内存的数据和代码就会被操作系统放进磁盘中
Linux操作系统的的状态
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磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
进程状态查看
ps aux / ps axj 命令
R状态--运行状态
#include <stdio.h> #include <unistd.h> int main() { int a=0; while(1) { a=1+2; }; return 0; }
S状态--阻塞状态
#include <stdio.h> #include <unistd.h> int main() { int a=0; while(1) { a=1+2; printf("当前a的值是:%d\n ",a); }; return 0; }
为什么是S状态?
因为printf会访问显示器,显示器是外设的输入输出速度比较慢,CPU就会等待显示器就绪。99%都是在等I/O就绪,1%才是执行打印代码。所以当我们去查的时候,几乎大概率都会是S状态
t状态 --停止状态
进程处于此状态表示该进程正在被追踪,比如 gdb 调试进程:
T状态--停止状态
测试代码
#include <stdio.h> #include <unistd.h> int main() { int a=0; while(1) { a=1+2; }; return 0; }
kill -l
kill -19 [pid]--停止进程
后台运行
kill -18 [pid]--运行进程
后台运行--‘+’的消失
前台运行--有‘+’
用Ctrl+c不能杀死
就用kill -9 [pid]
总结:
先将该进程暂停后,再运行。进程状态前面的 + 号消失了,该进程变成了后台程序。但是对于后台进程来说,我们只能通过 kill 命令来杀死它。
D状态--深度睡眠
深度睡眠TASK_UNINTERRUPTIBLE:不可被信号唤醒;
浅度睡眠TASK_INTERRUPTIBLE:唤醒方式,等到需要的资源,响应信号;
深度睡眠场景:
有些场景是不能响应信号的,比如读磁盘过程是不能打断的,NFS也是;
执行程序过程中,可能需要从磁盘读入可执行代码,假如在读磁盘过程中,又有代码需要从磁盘读取,就会造成嵌套睡眠。逻辑做的太复杂,所以读磁盘过程不允许打断,即只等待IO资源可用,不响应任何信号;
应用程序无法屏蔽也无法重载SIGKILL信号,深度睡眠可以不响应SIGKILL kill-9信号;
注意:处于深度睡眠状态的进程既不能被用户杀掉,也不能被操作系统杀掉,只能通过断电,或者等待进程自己醒来。深度睡眠一般只会在高IO的情况发生下,且如果操作系统中存在多个深度睡眠状态的程序,那么说明该操作系统也即将崩溃了。
X--死亡状态
死亡状态代表着一个进程结束运行,该进程对应的PCB以及代码和数据全部被操作系统回收。
Z--僵尸状态
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
两个特殊的进程
Z(zombie)-僵尸进程
僵尸进程是处于僵尸状态的进程
脚本代码
while :; do ps axj | head -1 && ps axj |grep myprocess |grep -v grep; sleep 1; done
测试代码
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t id = fork(); if(id == 0) { //child while(1){ printf("I am child process, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(5); exit(1); } } else { //parent while(1) { printf("I am parent proceass, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(1); } } return 0;}
子进程:pid= 26655
父进程:ppid=26654
当运行一段时间后的显现,26654进程因为打印变成S状态,由于26655退出了该进程变成僵尸进程。
在上面测试代码中
if(id == 0)
{
//child
while(1){
printf("I am child process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(5);
exit(1);
}
}
子进程通过5秒后退出,结合观察得到再子进程(26655)退出后,父进程(26654)变成了僵尸状态。
26654 26655 26654 5238 pts/0 26654 Z+ 1005 0:00 [myprocess] <defunct>
<defunct> 的意思失效
意味着该进程是失效的,死掉的
当我们再观察进程时,我们发现该进程不在了,其原因是关掉了父进程,该失效的子进程被系统回收了。
僵尸进程危害
进程的退出状态必须被维持下去,父进程需要一直知道子进程的状态,随时进行处理。可父进程如果一直不读取,那子进程就一直处于Z状态。
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!--内存泄漏
孤儿进程
测试代码
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t id = fork(); if(id == 0) { //child while(1){ printf("I am child process, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(1); } } else { //parent while(1) { printf("I am parent proceass, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(1); } } return 0;}
最开始代码跑起来时,父进程pid=8644,bash=5238;子进程的pid=8645,子进程的父进程8644;他们的运行状态都是S状态--因为在打印就会访问I/O;
当我们销毁子进程时,kill -9 8645;我们发现子打印进程不在了,但是子进程的状态还在,变成了Z状态,这个时候就只能等父进程退出,让操作系统进行回收。
当我们杀掉父进程时,子进程被操作系统领养了,这个过程就叫做孤儿进程
当整个进程变成孤儿了,我们发现我们用Ctrl + c 是不能退出的;我们细心就会发现最开始子进程是S+当变成孤儿进程了,它的状态就是S了。说明了该程序变成了后台程序。
那么这个时候我们就只有用kill -9 16866,杀掉子进程了
进程优先级
基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行 所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别
用top命令更改已存在进程的nice:
top
1.非root用户:sudo top 进入top
2.在top中按r
3.输入进程pid
4.输入nice值
这里是输入nice值为100,我们发现最大区间是99 -19
这里是输入nice值为-100,所以我们发现最小大区间是60-20
经过上面两个例子,我们就发现其实我们只能改变范围是-20至19,一共40个级别
我们再观察一个场景,当我们把nice值改9的时候,我们发现值变成89,那么就得出结论:每次改nice值都在默认nice值的基础上进行改动,而不是修改之后。
其他概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为 并发
进程切换
测试代码
#include <stdio.h> int main() { int a=10; int b=10; int c=b+a; printf("%d\n",c); a=20; b=30; c=b+a; printf("%d\n",c); return 0; }
我们从该代码中不难看出,a,b,c都是临时变量,在以前c语言也知道,他们的数据都是存放在寄存器里的,这里我们发现寄存器里的数据是可以发生改变的。这里我们进入vs2013查看反汇编,数据都是加载到寄存器中的。
在这里有一个概念需要知道:CPU虽然只有一套寄存器,但是寄存器的数据是属于当前进程的。这里的寄存器其实更偏向于寄存器内的数据,而不是寄存器硬件。
在这个过程中运行的时候,占有CPU进程不是一直要占有到进程结束!因为CPU虽然只有一套寄存器而且还要对其他进程进行处理,进程在运行的时候,都会有自己的时间片
操作系统会对每个进程进行设置一个时间片,让CPU去读取进程数据进行处理,就会有进入/退出--进程的切换。在这个切换的过程中寄存器数据需要被保护和恢复。
进程在切换的时候,要进行进程的上下文保护,当进程在恢复运行的时候,要进行上下文的恢复。
在任何时刻,CPU里面的寄存器里面的数据,看起来是在大家都能看到的寄存器上,但是,寄存器内的数据,只属于当前运行的进程!
寄存器被所有进程共享,寄存器内的数据,是每个进程各自私有的---上下文数据
环境变量
基本概念
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见环境变量
PATH : 指定命令的搜索路径
我们直接输入test我们发现,编译不起
我们将我们写的可执行程序放入usr/sbin路径中,需要注意的这个文件目录只有root用户可以进去
这种是不支持的,会污染指令池;
sudo rm /usr/bin/test.o ---删除
一般情况下,我们是选择用
exprot PATH=$PATH:
我们发现使用exprot PATH=$PATH: test.o 的路径还是在11-19中
总结:
1.PATH就是系统默认的搜索路径
2.系统的指令能被找到就是因为环境变量PATH本来就默认带了系统对于的路径搜索
3.which底层实现就使用环境变量PATH来进行路径搜索
系统会默认将.bash_profile执行一次,将环境变量导到shell中,也就是说环境变量的配置也就是.bash_profile 再启动的时候加载到bash中
vim .bash_profile--进入.bash_profile
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
用root和普通用户,分别执行 echo $HOME ,对比差异 . 执行 cd ~; pwd ,对应 ~ 和 HOME 的关系
SHELL : 当前Shell,它的值通常是/bin/bash。
和环境变量相关的命令
1. echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变
USER
测试代码
#include<stdio.h> #include <string.h> #include <stdlib.h> #define USER "USER" int main() { char *who =getenv(USER); if(strcmp(who,"root")==0) { printf("hello world\n"); printf("hello world\n"); printf("hello world\n"); printf("hello world\n"); } else{ printf("权限不足"); } return 0; }
这里说明不同的环境变量运用的场景不同
echo- export
测试代码
#include<stdio.h> #include <string.h> #include <stdlib.h> #define USER "USER" #define MY_ENV "myval" int main() { char *myenv=getenv(MY_ENV); if(NULL==myenv) { printf("%s,not found\n",MY_ENV); return 1; } printf("%s=%s\n", MY_ENV, myenv); return 0; }
我们自己定义一个变量,但是我们用echo 查看发现在当前文件能查看,但是用env去找不到。因为是这里相当于是定义的局部变量
本地变量只会在当前进程(bash)内有效
我们用export将本地变量导成环境变量
然后我们在运行test.o,我们发现myval可以被test.c引用
这里我们就得出结论:环境变量具有全局性,是会被子进程下去。
bash是一个系统进程,myval也是一个进程(fork),那么为了满足不同的场景,bash会帮助我们找指令路径,身份认证;
set
用set显示本地定义的shell变量和环境变
unset
env
其他相关指令
通过代码如何获取环境变量
命令行前两个参数
#include <stdio.h>#include <unistd.h>#include <string.h>#include <stdlib.h>#define USER "USER"#define MY_ENV "myval"#define MYPWD "PWD"int main(int argc, char *argv[]){ if(argc != 2) { printf("Usage: \n\t%s [-a/-b/-c/-ab/-bc/-ac/-abc]\n", argv[0]); return 1; } if(strcmp("-a", argv[1]) == 0) { printf("功能a\n"); } if(strcmp("-b", argv[1]) == 0) { printf("功能b\n"); } if(strcmp("-c", argv[1]) == 0) { printf("功能c\n"); } if(strcmp("-ab", argv[1]) == 0) { printf("功能ab\n"); } if(strcmp("-bc", argv[1]) == 0) { printf("功能bc\n"); } return 0;}
就好比同一个程序,有多个选项控制
命令行第三个参数
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #define USER "USER" #define MY_ENV "myval" #define MYPWD "PWD" int main(int argc, char *argv[],char *env[] ) { extern char **environ; int i=0; for ( i = 0; environ[i]; i++) { printf("%d:%s\n", i, environ[i]); } return 0; }
getenv
#include <stdio.h>#include <stdlib.h> int main(){ printf("%s\n", getenv("PATH")); return 0;}
在这个三个获取环境变量的方式:getenv,char*env,extren char **environ中一般推荐使用getenv;因为一般情况我们不需要把全部变量都获取,都是按照自己的需求来。
总结:
在操作系统中用来指定操作系统运行环境的一些参数,我们在启动系统的时候bash就会自动更新该数据,相应的环境变量都有相应的应用场景,一些简单的指令其实就是函数调用环境变量。
程序地址空间
用段代码感受父子进程地址
进程之间是相互独立,都有自己的pdb
测试代码
#include <stdio.h> #include <unistd.h> int global_value = 100; int main() { pid_t id = fork(); if(id < 0) { printf("fork error\n"); return 1; } else if(id == 0) { int cnt = 0; while(1) { printf("我是子进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value); sleep(1); cnt++; if(cnt == 10) { global_value = 300; printf("子进程已经更改了全局的变量啦..........\n"); } } } else { while(1) { printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value); sleep(2); } } } else { while(1) { printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value); sleep(2); } } sleep(1);}
运行这段代码后, 我们发现运行一段时间后,子进程和父进程的地址是一样的,但是他们global_value值却发生了改变,这个是为什么呢?
这里我需要想一下,这里地址没变,那么这个地址是什么?如果是物理地址父子进程的必须是一样,不然硬件是无法执行。那么这里就很显然就不是物理地址,就是我们提到的程序地址空间
为了解决父子进程地址一样,值不一样的现象,我们必须将程序地址空间搞清楚
感性理解程序地址空间
每个进程都会认为自己是操作系统的第一把手,只要操作系统批准自己随时都可以独占整个系统资源,又因为进程是独立,每个进程也不会知道自己会有其他进程,但是实际上是有多个进程需要操作系统进行分配,进程是不知道系统资源的多少。为了满足进程独占这个系统资源的需求,操作系统就给进程给了一个虚拟的地址空间(好比花了一个饼)。如果这是进程想全部的系统资源,这个时候操作是老大,他说不,进程还有没有能力取代他。
尽管操作系统就给进程花了一个饼,但是后面操作系统会将这个饼实现,就好像一个公司老板给员工们构建一个蓝图,最后将它实现。老板在画饼之前,可能会需要想好:公司会有好多人,会挣好多钱,在什么时间上市。这个过程在计算机中我们称之为描述,描述我们就可以用数据结构,使用结构体进行描述:
struct 蓝图{
char *who;
vchar *when:
char *target;
char *money;
//个岗位
}
那么操作系统对进程画的饼:是内核的一种数据结构:mm_struct
mm_struct{
内存空间的大小
}
程序地址空间是什么
我们在学c/c++的时候,指针指向的就是这虚拟地址空间,我们编写代码经过编译,链接后形成可执行文件,前面也讲过进程就是可执行程序的实例,里面设计到的地址,都是虚拟地址空间。
地址空间描述的基本空间大小是字节,32位下就是2的32次方个地址,2的32次方*1字节就等于4GB空间范围。每个字节都有唯一的地址。
程序地址空间--区域划分
如上图,有各种区:代码区,堆区,栈区等等,这些都是一段空间范围,假设:就好比堆区是0x0FFF1111--0x2FFFFFFF这个范围的地址空间。整个地址空间范围:0x00000000--0xFFFFFFFF
如果不是很好理解,在我们中小学时期,女生就很喜欢给同桌的男生画一个三八线,这个时候女生就相当于操作系统,给两个人都有使用的范围。比如一张课桌是100cm,我们用一把尺子来划分区域,女孩的区域是0,60,男孩的区域是60,100,那么计算机是怎么描述这个事情呢?
struct area
{
unsigned long start;
unsigned long end;
};
struct area girl = {0,50};
struct area boy = {50,100};
那么回到进程地址空间的每个区上,我们知道整个区都地址范围,还知道程序地址空间是用结构体描述的,那么我们就大概知道地址空间是什么了--如图。
mm_struct{
uint32_t code_start,code_end;
uint32_t date_start,code_end;
uint32_t head_start,code_end;
uint32_t stack_start,code_end;
}
Linux mm_struct 中关于区域划分的部分源码如下:
struct mm_struct
{
unsigned long code_start;//代码区
unsigned long code_end;
unsigned long init_start;//初始化区
unsigned long init_end;
unsigned long uninit_start;//未初始化区
unsigned long uninit_end;
unsigned long heap_start;//堆区
unsigned long heap_end;
unsigned long stack_start;//栈区
unsigned long stack_end;
//...等等
}
如果栈区想扩大,我们只需要控制该结构体的stack_start,stack_end就可以了,stack_star--区域的起始地址,stack_end--区域的结束地址。
所谓区域的调整:本质就是修改各个区域的end or start
为什么会存在进程地址空间
其实我们提到进程地址空间就感到有些迷糊,怎么突然就冒出两个地址空间了呢?一个进程地址空间,一个物理地址空间。他们之间有什么联系呢?进程地址空间是虚拟的,是假的,那它有什么用呢?为什么我们不直接写进物理地址空间呢?这里就展示一下他们大致的链接关系:
结论:
进程=内核数据结构+对应的的代码和数据
虚拟地址和物理空间之间是通过页表完成的映射关系
1、保证了数据的安全性。
我们为每一个进程都创建一个进程地址空间,然后通过页表来关联虚拟内存与物理内存,这样当我们用户对某一进程的虚拟内存越界访问或者非法读取与写入时,页表或操作系统可以直接进行拦截,从而保证了内存中数据的安全。这套规则是所有进程都需要遵守的。
2、进程地址空间可以更方便的进行不同进程间代码和数据的解耦,保证了进程的独立性。
对于互不相关的两个进程来说,它们都拥有自己独立的地址空间以及页表,页表会映射到不同的物理内存上,磁盘代码和数据加载到内存中的位置也不同,一个进程数据的改变不会影响另一个进程;
对于父子进程来说,由于子进程的 mm_struct 和页表是通过拷贝父进程得到的,所以二者指向同一块物理内存,共用内存中的同一份代码和数据,但即使是这样,父进程/子进程在修改数据是也会发生写时拷贝,不会影响另一个进程,保证了进程的独立性。
这里就回到最开始,就解决了子进程和父进程使用了同一个进程地址空间而数据不一样的问题:
如果进程直接访问物理内存,那么看到的地址就是物理地址,而语言中有指针,如果指针越界了,一个进程的指针指向了另一个进程的代码和数据,那么进程的独立性,便无法保证,因为物理内存暴露,其中就有可能有恶意程序直接通过物理地址,进行内存数据的篡改,如果里面的数据有账号密码就可以改密码,即使操作系统不让改,也可以读取。
3、进程地址空间让进程以统一的视角来看待磁盘代码以及各个内存区域,使得编译器也能够以相同的视角来进行代码的编译工作。
对于进程来说,各个进程都认为自己的数据被放置在对应的区域,比如代码区、全局数据区,但是物理内存实际上是可以非规律存储的;
对于磁盘中的程序以及编译器来说,编译器也是以进程地址空间的规则来进行编译的,所以磁盘中的可执行程序内部也是有地址的,且此地址也是虚拟地址;所以,当我们的程序被加载到内存变成进程后,不仅程序中的各个数据会被分配物理地址,程序的内部同时也存在虚拟地址,使得CPU在取指令进行运算时,拿到的下一条指令的地址也是虚拟地址,这样CPU也可以以 虚拟地址 -> 页表 -> 物理地址 的方式来统一执行工作。
注:严格来说,磁盘中程序内部的地址叫做逻辑地址,但是在上面我们就说过,对于Linux来说,虚拟地址、线性地址、逻辑地址是一样的,都是虚拟地址。