文章目录
进程的基本理解
进程的概念属于Linux系统编程的一部分,Linux系统编程要解决的问题有4点。
- 任务与任务之间的关系
- 任务与任务之间的同步
- 任务与任务之间的通信
- 任务与任务之间的作用
冯诺依曼体系结构
虽然进程属于计算机软件的概念,但是理解计算机硬件的基本组成对学习软件也有帮助。计算机硬件的基本结构是冯诺依曼体系结构。该结构认为计算机硬件可以大致被分为:输入设备、输出设备(I/O设备)、内存、CPU.其中CPU的核心是运算器和控制器。
CPU中的运算器可以进行运算,这里的运算不是常规的1+1=2,这种运算被称为算数运算,运算器不仅可以进行算数运算,也可以进行逻辑运算,判断真假也属于运算。这两种运算简称算罗运算。CPU中的控制器可以响应外部事件,起到控制作用。
内存
在冯诺依曼体系结构中有内存这个东西,I/O设备与CPU之间的交互都是通过内存完成的。原因:在冯诺依曼体系结构中,输入设备的本质是产生数据,输出设备的本质是保存数据,输入设备不能直接把数据交给CPU处理,输出设备也不能直接从CPU中拿数据,这个过程要通过内存间接完成,本质上是各种硬件的运算速度不同,CPU的运算速度要远远大于I/O设备,于是通过把数据转到内存,CPU直接从内存中拿数据,可以缩小因为硬件的运算速度差距造成的影响。冯诺依曼体系结构在输入设备、输出设备和CPU之间加了一个内存,提高了外设与CPU之间的交互速度,外设把数据加载到内存,CPU从内存中读取数据,经过运算,写回内存,数据再从内存加载到外设,大大提高了外设与CPU交互的速度。
离CPU越近的存储设备,速度越快,价格越高,越远的则相反。速度:CPU中的寄存器>内存>硬盘。CPU中的寄存器和内存属于掉电易失的存储介质,只要断电,里面的内容就容易丢失,硬盘则能长时间存储数据。
对于一个程序,想要运行它,必须要先加载到内存,这是由冯诺依曼体系结构决定的,因为CPU只和内存打交道。
操作系统的基本理解
进程管理是操作系统的核心之一,要理解进程,首先要对操作系统有一个大概的了解。
操作系统是一个对软硬件资源进行管理的软件。对上操作系统需要给用户提供一个良好的使用环境,对下操作系统要管理好软硬件资源,保证系统的稳定性。对软硬件资源的管理是手段,对上提供一个良好的使用环境是目的。
操作系统的管理
采用类比的方式理解操作系统的管理。
我们将学校中的人分为3种,校长,辅导员,学生。其中校长是管理者,学生是被管理者。管理者与被管理者之间可以不直接沟通,管理者只要能够拿到被管理者的数据,通过分析被管理者的数据进行某种决策,让辅导员执行该决策,就间接实现了管理的目的。校长对于学生的管理,不是对学生本身的管理,是对学生数据的管理。管理,是对被管理对象的数据进行管理
两个基本的问题:Q1:校长如何拿到学生的数据?Q2:校长的决策由谁执行?
校长通过辅导员拿到学生的数据,校长的决策由辅导员执行。辅导员是执行者。校长是操作系统,学生是底层的硬件,而辅导员则充当驱动程序的角色。操作系统是对软硬件进行管理的程序,校长管理辅导员(驱动程序,软件),也管理学生(硬件)。校长管理学生是进行先描述,在组织。校长可以将每一名学生定义为一个结构体对象。再把每一个结构体对象通过链表的形式链接在一起,这样对于学生的管理就变为了对链表的增删查改。
Linux内核是用C语言写的,Linux操作系统对硬件的管理本质上是对硬件的所有属性数据做管理。操作系统拿到硬件的数据,将其抽象为统一的数据结构,这样一来,对硬件的管理就变为对该数据结构的操作。
Linux操作系统的核心:内存管理,进程管理,驱动管理,文件系统。操作系统是假设所用用户都不值得信任的,用户不能直接访问操作系统,一是直接访问的方式成本太高,二是不安全。因此,操作系统只对外提供系统接口,我们可以调用这些接口,称为system call,系统调用。这些系统接口本质上就是用C语言写的函数。调用这些接口就是在调用函数。但是由于系统调用的接口使用起来成本也比较高,而且有些接口还和操作系统本身有关,所以在系统接口之上提供了图形化界面和shell外壳程序。相当于是对系统调用接口的封装,这些封装又被称为第三方库,把系统调用接口以第三方库的形式封装起来供使用,大大降低了使用难度。
进程
在Linux操作系统下,运行一条命令(ls,pwd),或者./a.out运行一个程序,在系统层面,就是创建了一个进程,当一个程序被加载到内存中时,严格来说已经不能被称为程序,应该被叫做一个进程。Linux同时存在大量的进程在系统中,需要对这些进程进行管理,如何管理?答案是先描述,在组织。
描述
当同时存在大量的进程在系统中时,Linux操作系统为了描述不同的进程,给每一个进程创建了他所对应的PCB结构体,全称是进程控制块。这个结构体里面记录了进程的所有属性。将每一个进程的PCB结构体用链表链接起来,这样对于进程的管理就变为对PCB结构体链表的增删查改,也就变成了对于一种具体数据结构的操作。
进程=对应的代码和数据+描述该代码和数据的PCB结构体,单纯的代码和数据严格来说不能被称为进程
PCB
PCB的全称是process control block,进程控制块。PCB是进程控制块的统称,在不同的操作系统下,进程控制块是不一样的,Linux系统下的PCB结构体是struct task_struct
.
task_struct
是Linux内核的一种数据结构,它会被装载到内存中并且里面包含进程的所有属性。
struct task_struct
{
标识符(pid,一个无符号整数):描述该进程的唯一符号,用来区分其他进程;
状态:任务状态,退出代码,退出信号;
优先级:该进程相对于其他进程的优先级;
程序计数器:程序中即将被执行的指令的下一条指令的地址;
内存指针:简单理解就是指向程序对应的代码和数据在内存中位置的指针;
上下文数据:进程执行时处理器的寄存器中的数据;
I/O状态信息;
记账信息:该进程被处理器所处理的总时间,等等;
......................................
};
组织进程
所有运行在Linux操作系统的中进程,他们的属性都是以task_struct链表的形式存在内核里面的,每个进程都有它的task_struct,每一个进程的task_struct最终都会以链表的形式链接到一起,通过对该链表的操作,实现对进程的管理。这个链表不是普通的单链表,是一种特殊的双向链表
查看进程
查看进程使用ps命令。
[slowstep@localhost ~]$ ps
PID TTY TIME CMD
2365 pts/0 00:00:00 bash
3473 pts/0 00:00:00 ps
//PID表示该进程的进程ID,bash这个进程的进程id是2365.
//bash是linux的shell外壳程序
运行ps,本质上也是创建进程,所以会有ps的进程id
ps常用的搭配
ps -ajx
ps -ajx | head -1 && ps -ajx | grep '关键字'
//&&表示左边的命令执行成功就指向右边的,否则就不执行右边的
Linux下所有的进程都在/proc这个目录下。
ls -al /proc
可以在该目录下找到当前的进程信息
ls -al /proc | grep '进程的id号'
获取当前进程的pid与ppid
NAME
getpid, getppid - get process identification
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
//pid_t 是无符号整数,相当于unsigned int
使用的函数是getpid和getppid,需要包含<sys/types.h>和<unistd.h>头文件。ppid是当前进程父进程的进程id
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id=getpid();
while(1)
{
printf("----------------\n");
printf("当前进程的id是%d\n",id);
printf("当前进程的父进程id是%d\n",getppid());
printf("----------------\n");
sleep(2);
}
return 0;
}
结果:
当前进程的id是4141
当前进程的父进程id是2365
ps -ajx | head -1 && ps -ajx | grep '1.out'
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2365 4141 4141 2365 pts/0 4141 S+ 1000 0:00 ./1.out
4087 4187 4186 4087 pts/1 4186 R+ 1000 0:00 grep --color=auto 1.out
可以看到pid为4141的进程的父进程id是2365
ls -al /proc | grep '2365'
dr-xr-xr-x. 9 slowstep slowstep 0 Aug 20 20:54 2365
ls -al /proc | grep '4141'
dr-xr-xr-x. 9 slowstep slowstep 0 Aug 20 22:28 4141
ps -ajx | head -1 && ps -ajx | grep '2365'
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2357 2365 2365 2365 pts/0 4141 Ss 1000 0:00 -bash
pid为2365的进程的ppid是2357,这个进程是Linux的shell外壳程序bash.我们平时运行的命令或者可执行程序都是以bash的子进程的方式去运行的
杀掉进程(kill)
kill -9 4141
----------------
当前进程的id是4141
当前进程的父进程id是2365
----------------
Killed
kill -l //可以查看所有可用的信号
创建子进程
创建子进程使用fork函数
NAME
fork - create a child process
SYNOPSIS
#include <unistd.h>
pid_t fork(void);
fork函数有两个返回值。
- fork函数创建子进程成功,给父进程返回子进程的pid,给子进程返回0
- 创建失败,返回-1
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("我是父进程,我的pid是%d\n",getpid());
pid_t id=fork();
if(id==-1)
{
perror("创建子进程失败:");
}
else if(id>0)
{
printf("创建子进程成功!我是父进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());
}
else if(id==0)
{
printf("我是子进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());
}
else //理论而言不可能有else
{
;
}
return 0;
}
结果
我是父进程,我的pid是4603
创建子进程成功!我是父进程,我的pid是4603,我的ppid是2365
我是子进程,我的pid是4604,我的ppid是1 #这里子进程的ppid=1的原因是子进程变为孤儿进程进行被1号进程领养
在fork函数之后,代码父子共享,实际上fork函数内部可以分为2个阶段,首先创建子进程,然后return id,也就是说,在return id之前,子进程已经创建完毕。return id以及其后的代码都是父子共享的,由于在return id以后父子进程的id值不一样,但是父子进程的id的虚拟地址是一样的,当虚拟地址一样而id值不一样会发生写时拷贝,映射到物理内存的不同区域,所以fork函数有2个返回值,并且父子进程打印出来的id虚拟地址是一样的。fork函数之后,代码父子共享,但是可以使用if/else控制父子能执行的代码,代码共享不代表都能执行.
进程调度
每一个进程都有它对应的task_struct结构体,进程调度相当于是在task_struct结构体形成的队列中挑选一个进程的过程,操作系统和CPU运行一个进程,就是从队列中挑选一个task_struct结构体,来执行它所对应的代码和数据。
动态检测进程
每隔一秒动态检测:
while : ;do ps -ajx | head -1 && ps -ajx | grep a.out | grep -v grep ;sleep 1;done
其中的a.out是可执行文件,执行起来以后对应一个进程。grep -v grep表示不匹配含有’grep’的信息
fork函数为什么这样返回?
fork函数给父进程返回子进程的pid,给子进程返回0,原因在于一个父进程可以有多个子进程,父进程需要管理好子进程,所以需要子进程的pid,而子进程只有一个父进程,父进程:子进程=1:n,给子进程正常返回0即可。
进程状态
对于进程的基本认识:
- CPU在执行进程任务时,是根据运行队列(run_queue)的先后顺序来的
- 进程调度是系统和CPU运行一个进程的过程
- fork()函数可以创建子进程,但是父子进程谁先运行是不知道的,由系统调度器决定,系统调度器可能先运行子进程,在运行父进程
- 当服务器压力过大时,OS可能杀掉一些进程,节省空间。
进程有不同的状态:
-
新建:当一个进程的task_struct没有进入运行队列时,称为新建状态。在Linux内核中没有这种状态,在Linux内核中,一个进程只要产生,就会立刻进入运行队列
-
运行:该进程的task_struct结构体正在运行队列中排队,进程还没有被执行。运行态,指的是task_struct在运行队列中排队,可能没有真正被运行。一个进程访问CPU要排队,访问磁盘,网卡,显卡等也要排队。访问CPU需要排队,这个队列称为运行队列,访问硬盘、显卡等需要排队,这些队列被称为阻塞队列。进程在排队等待CPU的资源的状态叫做运行态
-
阻塞:阻塞状态指的是排队等待非CPU资源的就绪。例如scanf函数,在等待输入设备的输入。下载文件时网络卡死,就是在等待网卡就绪。把这种等待非CPU资源就绪的状态叫做阻塞状态。
-
挂起:如果某一个进程在内存中,它的代码长时间没有被执行(原因可能是CPU需要执行的进程任务太多,暂时管不了这个进程),那么该进程占用了内存空间而且暂时没有被处理。此时,将该进程的代码和数据交换到磁盘的SWAP分区,等到CPU空闲了,有时间执行这个进程了,再把它的数据从SWAP分区转到内存。这种状态叫做挂起状态,即将长时间未被执行的进程的代码和数据交换到SWAP分区的状态。虽然处于挂起状态的进程它的代码和数据交换到了SWAP分区,但是它的task_struct结构体还在内存中,可以对该进程起到管理的作用。
使用ps命令可以查看到进程的不同状态。
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1947 3026 3026 1947 pts/0 3026 S+ 1000 0:00 ./a.out
STAT就表示进程的状态。
-
+ :加号表示前台进程,前台进程在运行的时候不能执行其他命令。没有加号的是后台进程,后台进程在运行的时候可以执行其他命令。
./a.out //前台进程 ./a.out & //后台进程
-
S :S表示休眠,对应阻塞状态,即该进程在等待某种非CPU资源的就绪,S表示的休眠状态可以中断,S状态的进程可以通过kill命令终止。也可能出现这样的情况:处于S状态的进程长时间在等待非CPU资源的就绪,而且还在内存中占用空间,如果此时CPU需要执行的进程任务太多,内存空间已经不够用了,那么操作系统会直接干掉这个S状态的进程,为其它进程在内存中腾出空间,即使这个S状态的进程在等待磁盘的写入数据。S状态的进程如果被操作系统干掉的话,可能会造成数据丢失等问题。
-
D :D状态也是睡眠状态,D状态被称为磁盘睡眠,是深度睡眠状态,即D状态的进程也是对应阻塞状态,也是在等待某种非CPU资源的就绪。但是D状态的进程不可被动唤醒,如果一个D状态的进程正在等待向磁盘写入数据,即使此时内存中存在大量需要被CPU执行的进程任务,操作系统也不能把D状态的进程杀掉,包括使用kill -9命令也无法杀掉D状态的进程。只有等到D状态的进程等待磁盘资源完毕,并且写入数据完毕,自动退出。D状态的进程除了自己退出,另外的办法就是关机重启,所以内存中不能有太多D状态的进程。使用dd命令可以演示D状态的进程
-
T(t) :T或者t表示进程处于暂停状态,这个最常见的场景就是使用gdb调试,打断点运行到某一位置停下,此时进程就处于暂停状态。S状态与T状态的区别是S状态的进程是在等待非CPU资源的就绪,T状态的进程是单纯的暂停。S状态是进程在等,T状态是进程暂停了,不动了。
-
X :X状态表示进程终止,dead,可以被立刻回收.X状态一般看不到,因为处于X状态的进程在一瞬间就被系统回收了(连同它的PCB)
-
Z :Z状态表示僵尸状态,指的是虽然进程已经退出,但是操作系统不能立刻回收,因为处于Z状态的进程退出原因比较蹊跷,需要让该进程的父进程或者操作系统检测一下这个进程,到底是因为什么原因退出的,只有当检测完毕之后,Z状态的进程才变为X,被父进程和OS回收。使用fork函数可以模拟出子进程的的Z状态。
当使用fork函数创建一个子进程时,如果子进程先执行完,子进程就会处于Z状态,此时,父进程必须调用wait函数或者waitpid函数来回收处于z状态的子进程。
Z状态进程与孤儿进程
-
Z状态的进程,可以认为它的PCB结构体还在内存中,但是它的代码和数据已经不在内存中了,因为Z状态的进程已经执行完了,只是等待它的父进程或者操作系统回收。如果Z状态的进程一直不被回收,就会造成内存泄漏。回收Z状态的进程,需要由父进程调用wait/waitpid函数。
#include<sys/types.h> //wait函数头文件 #include<sys/wait.h> pid_t wait(int* status);
当一个进程调用wait函数,这个函数就会自动分析当前进程是否存在某个子进程处于Z状态,如果存在,wait就会收集这个子进程的信息,并且把它彻底销毁后返回,如果没有找到处于Z状态的子进程,那么wait函数会一直阻塞,直到有一个处于Z状态的子进程为止。如果一个进程没有子进程,wait函数就会调用失败,返回-1.只要一个进程有子进程,并且调用了wait函数,那么wait函数就会一直等,直到等到子进程变为Z状态。然后wait函数自动捕获这个处于Z状态的进程,拿到它的pid并且销毁它。
#include<unistd.h> #include<sys/types.h> #include<sys/wait.h> #include<stdio.h> int main() { pid_t id=fork(); if(id==-1) { return 0; } else if(id==0) { for(int i=0;i<10;i++) { printf("I am child process,pid:%d,ppid:%d\n",getpid(),getppid()); sleep(1); } } else { printf("I am father,pid:%d\n",getpid()); pid_t id=wait(NULL); printf("捕获到了一个Z状态的进程,id是%d\n",id); } return 0; }
[slowstep@localhost mydir]$ ./a.out I am father,pid:10530 I am child process,pid:10531,ppid:10530 I am child process,pid:10531,ppid:10530 I am child process,pid:10531,ppid:10530 I am child process,pid:10531,ppid:10530 I am child process,pid:10531,ppid:10530 I am child process,pid:10531,ppid:10530 I am child process,pid:10531,ppid:10530 I am child process,pid:10531,ppid:10530 I am child process,pid:10531,ppid:10530 I am child process,pid:10531,ppid:10530 捕获到了一个Z状态的进程,id是10531 //3~12行wait函数一直在等待
-
孤儿进程:孤儿进程指的是一个进程自身还在跑,但是它的父进程已经结束了,这样的话,最后这个进程找不到它的父进程,一旦该进程指向结束,会一直处于Z状态,没有它的父进程来回收它,这种进程叫做孤儿进程。一般孤儿进程会被1号进程领养,1号进程的名字是init,就是操作系统。即孤儿进程会被操作系统领养。
#include<stdio.h> #include<unistd.h> #include<sys/types.h> int main() { printf("父进程的pid是:%d\n",getpid()); pid_t id=fork(); if(id==-1){ printf("子进程创建失败!\n"); return 0; } else if(id>0){ int cnt=3; while(cnt>=0) { printf("我是父进程,我的pid是%d\n",getpid()); cnt--; sleep(1); } } else{ int cnt=5; while(cnt>=0) { printf("我是子进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid()); cnt--; sleep(1); } } return 0; }
结果:
父进程的pid是:16198 我是父进程,我的pid是16198 我是子进程,我的pid是16199,我的ppid是16198 我是父进程,我的pid是16198 我是子进程,我的pid是16199,我的ppid是16198 我是子进程,我的pid是16199,我的ppid是16198 我是父进程,我的pid是16198 我是子进程,我的pid是16199,我的ppid是16198 我是父进程,我的pid是16198 我是子进程,我的pid是16199,我的ppid是16198 [slowstep@localhost 循环队列]$ 我是子进程,我的pid是16199,我的ppid是1
父进程先执行完,子进程才跑完,当父进程和子进程都在跑的时候,子进程的ppid是16198,一旦父进程跑完,子进程就变为孤儿进程,被1号进程领养。此时,子进程的ppid变为1.
优先级
进程与进程之间存在优先级的概念。由于CPU只有一个,而进程有多个,于是就产生了哪一个进程先享受CPU资源,哪一个进程后享受CPU资源的问题。进程的优先级是由调度器进行评判的。
进程的优先级=老的优先级+nice值,nice值的范围是[-20,19],在计算进程的优先级时,每一次老的优先级都是80.优先级越小,进程就越优先被执行。nice值越小,进程的优先级值越小,但是越先被执行。
查看优先级:
ps -la
ps -la
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 16435 14758 0 80 0 - 3307 hrtime pts/0 00:00:00 a.out
PRI表示优先级,NI表示nice值
更改nice值。
1.top
2.r
3.输入pid
4.输入nice值
进程的性质
-
进程具有独立性,一个进程出现问题基本不会影响另外一个进程。包括父子进程之间也有独立性,父子进程可能代码共享,但是不会相互影响,即父进程没了,子进程该怎么执行怎么执行。只不过此时子进程被1号进程领养了
-
进程具有竞争性,即进程与进程之间会竞争CPU资源。
-
进程有并发和并行
并行与并发:并行指的是有多个CPU,多个进程同时在跑。并发指的是只有一个CPU,每个进程有自己的时间片,把自己的时间片跑完了就要从CPU上下来。并发的进程存在抢占与出让的行为,一个优先级高的进程可能抢占一个优先级低的进程,即使这个进程的时间片还没有跑完。一个进程如果运行完了还没有达到自己的时间片,会出让CPU.
并行的CPU各自也是并发的。
对于并发的进程,该进程在被CPU执行时,一定要把自己的数据放到CPU中的寄存器中,CPU中的寄存器保存该进程的临时数据。这个数据被称为进程的上下文数据。当一个进程的时间片跑完了,他要带走自己的上下文数据。等到下次在享受CPU资源时,再把上下文数据加载到寄存器中,寄存器只有一个,但是不同的进程有它们各自的上下文数据,上下文数据有多份。进程在运行的时候,寄存器中保存的一定是临时数据,进程的上下文数据一定不能删除,否则等到下次cpu在执行的时候不知道从哪一个位置开始执行代码了。
. 进程具有竞争性,即进程与进程之间会竞争CPU资源。
-
进程有并发和并行
并行与并发:并行指的是有多个CPU,多个进程同时在跑。并发指的是只有一个CPU,每个进程有自己的时间片,把自己的时间片跑完了就要从CPU上下来。并发的进程存在抢占与出让的行为,一个优先级高的进程可能抢占一个优先级低的进程,即使这个进程的时间片还没有跑完。一个进程如果运行完了还没有达到自己的时间片,会出让CPU.
并行的CPU各自也是并发的。
对于并发的进程,该进程在被CPU执行时,一定要把自己的数据放到CPU中的寄存器中,CPU中的寄存器保存该进程的临时数据。这个数据被称为进程的上下文数据。当一个进程的时间片跑完了,他要带走自己的上下文数据。等到下次在享受CPU资源时,再把上下文数据加载到寄存器中,寄存器只有一个,但是不同的进程有它们各自的上下文数据,上下文数据有多份。进程在运行的时候,寄存器中保存的一定是临时数据,进程的上下文数据一定不能删除,否则等到下次cpu在执行的时候不知道从哪一个位置开始执行代码了。