从操作系统谈起
我们都知道操作系统实际上是一款搞软硬件资源管理的软件,而事实上,操作系统在管理任何对象时,都需要先对对象进行描述(使用结构体描述),然后再对对象进行组织(使用链表),所以对于我们本节将讲到的进程,操作系统同样使用先描述再组织的方式来管理进程。
1.进程的基本概念
基本概念:
进程可以简单理解为正在执行(将代码数据加载到内存)的一个程序,从内核的角度来说,进程是一个需要分配资源的实体。程序和进程的根本区别在于程序是一个硬盘上的文件,而进程被加载到内存除了有相应的代码和数据,为了让操作系统便于管理他还拥有进程控制块(pcb)
1.1描述进程PCB
谈什么是pcb之前我们先举一个栗子,你现在是一位在校的大学生,学校的教务处为了能更好的管理学生,定义了一个描述学生的struct结构体,里面存放了学生的姓名,性别年龄,学号,学习每招收一个新的学生就用此结构体为他创建一个结构体变量,这些变量用一个链表链接管理起来,每招收一个新学生的结构体变量就插入链表,如果开除一个学生将这个学生的变量从链表中删除。。。
上面的这个例子为了告诉读者,相同的道理,为了描述进程,操作系统像描述学生那样使用结构体描述进程,而pcb就是描述进程的结构体,pcb也叫进程控制块,而用结构体就是前面说到的那种描述行为,使用链表链接就是组织行为。所以我们现在一谈到进程首先应该想到进程的pcb。
1.2linux下的PCB
上面讲到的pcb是进程属性的集合,而再linux下的pcb叫做test_struct,task_struct是Linux内核的一种数据结构,它会被装载到内存里并且包含着进程的信息,linux下进程都被双链表组织起来。
test_struct的结构如下:http://www.cnblogs.com/tongyan2/p/5544887.html
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;
};
1.3test_struct内容分类
上面给大家列出了结构体的内容,这里挑几个比较有用的讲解:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。比如fork()函数返回的pid
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址,有pc/ip。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程的硬件上下文信息,程序运行时会在寄存器中存储临时变量,如果程序没有执行完需要使用此信息恢复。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
1.4查看/杀死一个进程
查看的进程的代码如下:
1 #include<iostream>
2 #include<unistd.h>
3 using namespace std;
4
5 int main()
6 {
7 while(1)
8 {
9 cout << "im process.."<<endl;
10 sleep(1);
11 }
12 return 0;
13 }
我们让程序在vm下跑起来
在xshell下输入命令:ps aux | grep ‘myprocess’
我们就拿到了图中所显示的进程,而下面那一条是grep的进程,因为他也是一个程序。
换一个命令:ps aux | head -1 && ps aux | grep ‘myprocess’ 此时我们多打印出的这一行就看到了哪个是pid(上面讲过的标识符)这里的pid是9314,但是如果杀死进程再次执行程序这个pid可能会改变
输入命令:kill -9 pid 杀死进程
vm下的进程已经被杀死:
但是我们凭什么说他是一个进程呢?接着来看:
输入命令:ls /proc
proc下放的是一些操作系统下的动态文件,所以我们跑起来的进程pid会放在这个文件下:
ps:当然,这里还有一条命令叫top(直接输入top),这是linux的任务管理器。
1.5获取当前进程的pid/ppid
先来谈谈什么是父子进程:你可以理解为,理发店的师傅去进行给顾客剪头发,但是他发现自己每天的顾客太多,所以他招了很多学徒,理发店师傅剪发就叫做父进程,学徒剪发就叫做子进程。而在linux下,我们所写的程序都是父进程创建出来的(bash)
- pid:子进程的标识符
- ppid:父进程的标识符
首先我们写一段可以获取pid/ppid的代码:
getpid()/getppid()函数返回值为pid_t类型
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/types.h>
4 using namespace std;
5
6 int main()
7 {
8 while(1)
9 {
10 cout << "im process..,pid:"<<getpid()\
11 <<" " <<"ppid:"<<getppid()<<endl;
12 sleep(1);
13 }
14 return 0;
15 }
下面给大家看看代码跑起来的现象:我们发现这里的ppid始终为8776
用前面的指令来查看一下着这个ppid:
我们看到这个bash是用来命令行解释的外壳程序,这个父进程的ppid在重启时可能发现变化。
1.6初始fork()
我们先来man一下这个函数:
描述里说这是一个创建一个子进程的一个函数
返回值:比较模糊,这里的大体意思是函数给父进程返回子进程的pid,给子进程返回0
我们先执行下面代码:
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/types.h>
4 using namespace std;
5
6 int main()
7 {
8 while(1)
9 {
10 fork();//增加一句fork()函数
10 cout << "im process..,pid:"<<getpid()\
11 <<" " <<"ppid:"<<getppid()<<endl;
12 sleep(1);
13 }
14 return 0;
15 }
子进程:10337(是10336的子进程) 10336(是10337的父进程)
父进程:8776(bash) 10336(是10337的父进程)
所以这里足以证明fork函数可以创建进程。
为了真正意义上看到俩个进程同时执行代码:
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/types.h>
4 using namespace std;
5
6 int main()
7 {
8 pid_t id = fork();
9 if(id == 0)
10 {
11 while(1)
12 {
13 cout << "im child...."<<endl;
14 sleep(1);
15 }
16 }
17 else
18 {
19 while(1)
20 {
21 cout << "im father...."<<endl;
22 sleep(2);
23 }
24 }
结果父子进程同时运行了起来:
2.进程状态
一起先来看看进程状态有哪些:
- R运行状态:并不代表一定在运行中,他代表要不在运行中,要不在运行队列中
- S睡眠状态:进程等待事件完成,也叫可中断睡眠
- D磁盘休眠状态:无法终止的状态,一般在一些i/o的过程中存在(访问外设时)
- T停止状态:表示进程状态是停止
- X死亡状态:只是一个返回状态,并不会在状态列表中看到他
- Z僵尸状态:一个进程退出,相关的pcb不会被立即释放,等待操作系统读取信息,这个状态叫做僵尸状态
2.1僵尸进程
我们前面已经讲过僵尸进程的形成原理:
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
我们下面的代码验证僵尸进程:
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<stdlib.h>
5 using namespace std;
6
7 int main()
8 {
9 pid_t id = fork();
10 if(id == 0)
11 {
12 int count = 0;
13 while(1)
14 {
15 cout << "im child...."<<endl;
16 sleep(1);
17 if(count++ >=5)
18 {
19 exit(1);
20 }
21 }
22 }
23 else
24 {
25 while(1)
26 {
27 cout << "im father...."<<endl;
28 sleep(2);
29 }
30 }
31 return 0;
32 }
我们在xshell下输入这段命令当作监视(每秒打印查看进程命令)
前五秒:父子进程都是s状态(这里不是r状态因为他们都在睡眠)
五秒后:子进程变成僵尸状态
僵尸进程的危害
- 进程的退出状态必须被维持下去,可父进程如果一直不读取,那子进程就一直处于Z状态
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
- 那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,对象本身就要占用内存,从而造成内存泄漏
2.2孤儿进程
- 父进程先退出,子进程就称之为“孤儿进程”
- 父进程退出后,孤儿进程被1号init进程领养
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<stdlib.h>
5 using namespace std;
6
7 int main()
8 {
9 pid_t id = fork();
10 if(id == 0)
11 {
13 while(1)
14 {
15 cout << "im child...."<<endl;
16 sleep(1);
17
21 }
22 }
23 else
24 {
12 int count = 0;
25 while(1)
26 {
27 cout << "im father...."<<endl;
28 sleep(2);
if(count++ >=5)
18 {
19 exit(1);
20 }
29 }
30 }
31 return 0;
32 }
五秒前:
五秒后:子进程变成孤儿进程,父进程被自己的父进程(bash)回收
3.进程优先级
输入命令:ps -l
- UID:执行者的身份
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值,这个值与PRI相加的值就是最后进程优先级的值,最低 - 20,最高19
3.1PRI vs NI
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化,可以理解nice值是进程优先级的修正修正数据。
如何修正nice值:top命令下按r并且输入进程pid可以修改
我们修改后的nice值加pri的值就是执行优先级的值,但是修改nice值需要root超级用户权限,通常情况下我们不要去修改nice值,因为那不是普通用户该干的事。
优先级的其他概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高-效完成任务,更合理竞争相关资源,便具有了优先级(存量博弈)
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
4.环境变量
环境变量:环境变量是指在操作系统中具有某些特殊作用的全局变量
4.1常见的环境变量
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash。
PATH
查看环境变量:echo $PATH 命令
可以看到我们的程序跑起来时必须指定路径,但是系统的命令(ls,pwd…)不用,是因为PATH这个环境变量已经指定了搜索路径
如果我们希望自己的命名也不使用添加路径可以将我们的程序名字添加到PATH指定的任意一个文件下:cp myprocess /usr/bin 但是这样我们会污染系统命令,我们如果既不想添加到系统命令集中,但是又不想指明路径,这时候我们可以将自己程序所在的路径放到环境变量PATH里
执行:export PATH=$PATH:你的程序所在路径
执行后你就发现echo $PATH可以看到自己文件的所在路径,在这个文件下执行程序不需要指明路径。
HOME
这个环境变量比较简单,是指定用户的主工作目录:
SHELL
shell指的是命令行解释器,所以查看此环境变量就显示当前的SHELL解释器:
4.2和环境变量相关的命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
4.3环境变量的组织方式
main函数的第三个参数,是一个环境变量的数组:(第一个参数是命令个数,第二个是存放命令参数的数组)
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
执行此程序会打印一些字符串,大家可以试试之前讲过的env命令和这段程序打印字符串的比较:这里太长没打印完
这些环境变量是系统调用main函数时,系统传给main函数的环境变量,我们也可以使用下面代码打印:
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
所以环境变量的组织方式如下图:
ps:环境变量是全局变量,可以被子进程继承
总结
- 我们这里了解了什么是进程,系统是怎么管理进程的(pcb结构体),进程如何创建,进程的状态和进程优先级一些基础概念,但是要真的做到完全了解进程还是需要进一步的学习