Linux进程
一、进程相关概念
1、进程定义
狭义定义:进程是正在运行的程序的实例。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
- 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
- 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈区(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
2、进程的特性
-
**动态性:**进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的;
-
**并发性:**任何进程都可以同其他进程一起并发执行;
-
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
-
**异步性:**由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的程序集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
3、进程、线程与程序
-
程序:程序并不能单独执行,是静止的,只有将程序加载到内存中,系统为其分配资源后才能够执行。
-
进程:程序对一个数据集的动态执行过程,一个进程包含一个或者更多的线程,一个线程同时只能被一个进程所拥有,进程是分配资源的基本单位。进程拥有独立的内存单元,而多个线程共享内存,从而提高了应用程序的运行效率。
-
线程:线程是进程内的基本调度单位,线程的划分尺度小于进程,并发性更高,线程本身不拥有系统资源,但是该线程可与同属进其他线程共享该进程所拥有的全部资源。每一个独立的线程,都有一个程序运行的入口、顺序执行序列和程序的出口。
在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。
-
进程和线程的区别
1.一个进程可以包含至少一个线程,一般来说也就是主线程,而一个线程只能属于一个进程;
2.进程拥有独立的内存,而线程没有独立的资源空间, 只是暂时存储在计数器,寄存器,栈中,同一个进程间的线程可以共享资源;
3.将代码放入到代码区之后,进程产生,但还没执行,我们所说的执行一般是是主线程main函数开始执行;
4.进程比线程更加消耗资源;
5.进程对资源的保护要求高,而线程要求不高;
6.进程是处理器这一层面的抽象,而线程是进程的基础上进一步并发的抽象;
7.同一个进程下,一个线程的挂掉,会导致整个进程的挂掉,而进程之间不会相互影响。
-
进程和程序的区别
1.程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态的概念;
2.程序可以作为一种软件资料长期存在,而进程是有一定生命期的。程序是永久的,进程是暂时的;
3.进程更能真实地描述并发,而程序不能;
4.进程具有创建其他进程的功能,而程序没有;
5.同一程序同时运行于若干个数据集合上,它将属于若干个不同的进程,也就是说同一程序可以对应多个进程;
6.在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单元都是进程。
4、进程的状态
- 就绪状态(Ready):
进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
- 运行状态(Running):
进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
- 阻塞状态(Blocked):
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。
5、Linux环境下查看系统中的进程
-
使用ps aux指令查看所有的进程,在实际工作中可以配合grep查找相关进程:ps aux|grep 进程名;
-
使用top指令查看,类似windows任务管理器。
二、进程控制
1、进程标识:pid
每个进程都有一个非负整型表示的唯一进程ID。虽然是唯一的,但是进程ID是可复用的:当一个进程终止后,其进程ID就成为复用的候选者。大多数系统会使用延迟复用算法,使得赋予新建进程ID不同于最近进程所使用的ID,这防止了将新进程误认为是使用同一ID的某个已终止 的先前进程。
系统中有一些专用进程:
- ID为0的进程通常是调度进程,常被称为交换进程(swapper)。该进程是内核的一部分,不执行任何磁盘上的程序,因此也被称为系统教程。
- ID为1的进程通常是init进程,在自举过程结束时由内核调用。init通常读取与系统有关的初始化文件,并将系统引导到一个状态(如多用户)。init进程绝不会终止,他是一个普通的用户进程(与交换进程不同,他不是内核中的系统进程),但是它以超级用户特权运行。下文部分会说明init如何成为所有孤儿进程的父进程。
除了进程ID,每个进程还有一些其他标识符:
#include<unistd.h>
pid_t getpid(void);//返回值:调用进程的进程ID
pid_t getppid(void);//返回值:调用进程的父进程ID
pid_t getuid(void);//返回值:调用进程的实际用户ID
pid_t geteuid(void);//返回值:调用进程的有效用户ID
pid_t getgid(void);//返回值:调用进程的实际组ID
pid_t getegid(void);//返回值:调用进程的有效组ID
2、创建进程
A进程如果创建了B进程,A进程就是B进程的父进程,B进程就是A进程的子进程。
(1)fork函数
在Linux中,我们通常使用fork函数来为一个已经存在的进程创建一个新进程。而这个新创建出来的进程被称为原进程的子进程,原进程被称为该进程的父进程。
#include <unistd.h>
pid_t fork(void);
/*fork函数调用成功,返回两次:子进程返回值为0,父进程返回子进程pid(非负数),代表当前进程为父进程;
调用失败,返回-1*/
fork在创建一个进程后,子进程和父进程继续执行fork调用之后的指令,子进程是父进程的副本。子进程会复制父进程的PCB,二者之间代码共享,数据独有,拥有各自的进程虚拟地址空间。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = getpid();
fork();
printf("my pid = %d,curent pro id = %d\n",pid,getpid());
return 0;
}
/*运行结果
my pid = 6569,curent pro id = 6569
my pid = 6569,curent pro id = 6570*/
既然代码共享,并且子进程是拷贝了父进程的PCB,虽然他们各自拥有自己的进程虚拟地址空间,但其中的数据必然是相同的(拷贝而来),并且通过页表映射到同一块物理内存中,那么又如何做到数据独有呢?答案是:通过写时拷贝技术。
写时拷贝技术:
- 内核只为新生成的子进程创建虚拟空间结构,它们来复制父进程的虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的物理空间:
- 当父子进程任意一方要对数据进行修改时,都可能会对另一方造成影响,上面又说到任意进程之间是具有独立性的,不会互相影响,那么这时操作系统就会介入,**会给父进程和子进程重新在物理内存中开辟一块空间,并将数据拷贝过去。父子进程也不再指向原来的那一份数据,而是指向修改拷贝的这一份数据。**这就叫做写实拷贝。这样避免了直接给子进程重新开辟内存空间,造成内存数据冗余。
下面我们来观察一下数据在父子进程中改变后的现象:
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
int data = 10;
printf("data = %d, data adrress = %p\n",data,&data);
pid = fork();
if(pid < 0){
printf("fork error!\n");
return -1;
}
if(pid == 0){
data = data + 100;
printf("chiled pid = %d, chiled data = %d, data adrress
= %p\n",getpid(),data,&data);
}
if(pid > 0){
data = data + 300;
printf("father pid = %d, father data = %d, data adrress
= %p\n",getpid(),data,&data);
}
return 0;
}
/*运行结果
data = 10, data adrress = 0x7fff1a8d9878
father pid = 8542, father data = 310, data adrress = 0x7fff1a8d9878
chiled pid = 8543, chiled data = 110, data adrress = 0x7fff1a8d9878*/
观察结果发现,虽然data数据在父子进程中都发生了改变,但是对应的虚拟地址都是相同的。
子进程拷贝父进程的PCB,拥有和父进程一模一样的进程虚拟地空间以及数据,但父进程和子进程将自己
的data更改后,会在物理内存中为其重新开辟空间来存储进程更改后的数据,而结果中看到的地址完全相
同,则是因为它们仅仅是虚拟的地址空间,真正的值是存储在物理内存中的。而这时通过页表的映射,这
俩个看似相同的地址已经指向了不同的物理内存。
fork函数有以下两种用法:
- 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的-父进程等待客户端的服务请求,当这种请求到达时,父进程调用fork,使子进程处理此请求,父进程则继续等待下一个服务请求。
- 一个进程要执行一个不同的程序。这对shell是常见的情况,在这种情况下,子进程从fork返回后立即调用exec。
(2)vfork函数
除了fork函数,vfork也同样是用来创建子进程的系统调用函数,并且返回值及其含义和fork也相同。
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
vfork和fork的区别:
vfork和fork同样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit,于是不会引用复制的虚拟空间,但是在子进程调用exec或exit之前,它直接在父进程的空间中运行。另外,vfork会保证子进程先运行,在子进程调用exec或exit之后父进程才可能被调度运行,当子进程调用这exec或exit任意一个时,父进程会恢复运行。即:
- vfork 直接使用父进程存储空间,不拷贝。
- vfork保证子进程先运行,当子进程调用exec或exit退出后,父进程才执行。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
int data = 10;
printf("data = %d, data adrress = %p\n",data,&data);
pid = vfork();
if(pid < 0){
printf("fork error!\n");
return -1;
}
if(pid == 0){
sleep(3);
data = data + 100;
printf("chiled pid = %d, chiled data = %d, data adrress
= %p\n",getpid(),data,&data);
_exit(0);
}
if(pid > 0){
data = data + 300;
printf("father pid = %d, father data = %d, data adrress
= %p\n",getpid(),data,&data);
}
return 0;
}
/*运行结果
data = 10, data adrress = 0x7fffece5c438
chiled pid = 9283, chiled data = 110, data adrress = 0x7fffece5c438
father pid = 9282, father data = 410, data adrress = 0x7fffece5c438*/
观察结果发现:
在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法,但在
vfork中虽然子进程使用sleep函数使子进程休眠三秒,但依然是子进程先输出结果,然后父进程才
打印结果,即vfork会保证子进程先执行。
data数据在子进程中更改之后,父进程使用的data数据是子进程修改过的值,即子进程直接在父进程
的存储空间。
3、进程终止
有八种方式使进程终止,其中有五种为正常终止:
- Main函数调用return;
- 进程调用exit(),标准c