目录
1、进程的定义
程序和进程的区别:
程序:就是磁盘上的可执行文件文件,并且只占用磁盘上的空间,是一个静态的概念。
进程:被执行之后的程序叫做进程,不占用磁盘空间,需要消耗系统的内存,CPU资源,每个运行的进程的都对应一个属于自己的虚拟地址空间,这是一个动态的概念。
2、进程的状态及转换
进程整个生命周期可以简单划分为三种状态:
就绪态:进程已经具备执行的一切条件,正在等待分配CPU的处理时间。
执行态:该进程正在占用CPU运行。
等待态:进程因不具备某些执行条件而暂时无法继续执行的状态。
进程的调度机制:
时间片轮转,上下文切换,多进程不是说一个进程执行完再执行另一个进程,而是交替执行的,一个进程执行一段时间,然后下一个进程在执行一段时间,依次类推,所有进程执行完之后再回到第一个今年初继续执行以此类推。
3、进程控制块
进程控制块就是用于保存一个进程信息的结构体,又称之为PCB
OS是根据PCB来对并发执行的进程进行控制和管理的。系统在创建一个进程的时候会开辟一段内存空间存放与此进程相关的PCB数据结构。
PCB是操作系统中最重要的记录型数据结构。PCB中记录了用于描述进程进展情况及控制进程运行所需的全部信息。
PCB是进程存在的唯一标志,在Linux中PCB存放在task_struct结构体中。
4、进程号
每个进程都由一个进程号来标识,其类型为pid_t,进程号的范围:0~32767
进程号是由操作系统随机给当前进程分配的,不能自己控制
进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用了
在ubuntu中查看当前系统中所有的开启的进程
ps ajx
PPID:当前进程的父进程的进程号
PID:当前进程的进程号
PGID:当前进程所在的组的进程组ID
COMMAND:当前进程的名字
特殊的进程号:
在linux系统中进程号由0开始。
进程号为0及1的进程由内核创建。
进程号为0的进程通常是调度进程,常被称为交换进程(swapper)。
进程号为1的进程通常是init进程,init进程是所有进程的祖先。
除调度进程外,在linux下面所有的进程都由进程init进程直接或者间接创建
Linux操作系统提供了三个获得进程号的函数getpid()、getppid()、getpgid()。
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:获取当前进程的进程号
pid_t getppid(void);
功能:获取当前进程的父进程的进程号
pid_t getpgid(pid_t pid);
功能:获取当前进程所在进程组的id
5、进程的创建--fork函数
#include <unistd.h>
pid_t fork(void);
功能:在已有的进程基础上有创建一个子进程
参数: 无
返回值:
成功:
>0 子进程的进程号,标识父进程的代码区
=0 子进程的代码区
失败:
‐1 返回给父进程,子进程不会创建
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间
地址空间:
包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号
等。
子进程所独有的只有它的进程号,计时器等,因此,使用fork函数的代价是很大的。
fork函数执行完毕后父子进程的空间示意图:
6、进程的创建
- 通过fork函数的返回值来区分父子进程的独立的代码区,大于0为父进程,等于0为子进程。
- 父子进程是来回交替执行的,谁先运行,谁后运行是不确定的,不要认为父进程执行完之后才会执行子进程。
int main()
{
// 在父进程中创建子进程
pid_t pid = fork();
printf("当前进程fork()的返回值: %d\n", pid);
if(pid > 0)
{
// 父进程执行的逻辑
printf("我是父进程, pid = %d\n", getpid());
}
else if(pid == 0)
{
// 子进程执行的逻辑
printf("我是子进程, pid = %d, 我爹是: %d\n", getpid(), getppid());
}
else // pid == -1
{
// 创建子进程失败了
}
// 不加判断, 父子进程都会执行这个循环
for(int i=0; i<5; ++i)
{
printf("%d\n", i);
}
return 0;
}
子进程继承父进程的空间
- 父进程是从 main () 函数开始运行的,子进程是在父进程中调用 fork () 函数之后被创建,子进程就从 fork () 之后开始向下执行代码。
int main(int argc, char *argv[])
{
int fd;
if((fd = open("file.txt", O_RDONLY)) == -1)
{
perror("fail to open");
return -1;
}
//子进程会继承父进程的一些公有的区域,例如磁盘空间,内核空间
//文件描述符的偏移量保存在内核空间中,所以父进程改变偏移量,则子进程获取的偏移量是改变之后的
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("fail to fork");
return -1;
}
if(pid > 0)
{
printf("This is a parent process\n");
char buf[32] = "";
if(read(fd, buf, 30) == -1)
{
perror("fail to read");
return -1;
}
printf("buf = [%s]\n", buf);
}
else
{
sleep(1);
printf("This is a son process\n");
char buf[32] = "";
if(read(fd, buf, 30) == -1)
{
perror("fail to read");
return -1;
}
printf("buf = [%s]\n", buf);
}
while(1)
{
}
return 0;
}
7、exit和_exit函数
如果想要直接退出某个进程可以在程序的任何位置调用 exit() 或者_exit() 函数。函数的参数相当于退出码,如果参数值为 0 程序退出之后的状态码就是 0, 如果是 100 退出的状态码就是 100。
exit函数
#include <unistd.h>
void _exit(int status);
功能:退出当前进程
参数:
status:退出状态,由父进程通过wait函数接收这个状态
一般失败退出设置为非0
一般成功退出设置为0
返回值:
无
_exit函数
include <stdlib.h>
void exit(int status);
功能:退出当前进程
参数:
status:退出状态,由父进程通过wait函数接收这个状态
一般失败退出设置为非
一般成功退出设置为0
返回值:
无
exit和_exit函数的区别:
- exit为库函数,而_exit为系统调用
- exit会刷新缓冲区,但是_exit不会刷新缓冲区
- 一般会使用exit
8、进程的回收
为了避免僵尸进程的产生,一般我们会在父进程中进行子进程的资源回收,回收方式有两种,一种是阻塞方式 wait(),一种是非阻塞方式 waitpid()。
1、wait函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
功能:等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
调用wait函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒。
若调用进程没有子进程或它的子进程已经结束,该函数立即返回。
参数:
status:函数返回时,参数status中包含子进程退出时的状态信息。
子进程的退出信息在一个int中包含了多个字段,
用宏定义可以取出其中的每个字段
子进程可以通过exit或者_exit函数发送退出状态
返回值:
成功:子进程的进程号。
失败:‐1
取出子进程的退出信息
WIFEXITED(status)
如果子进程是正常终止的,取出的字段值非零。
WEXITSTATUS(status)
返回子进程的退出状态,退出状态保存在status变量的8~16位。
在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏。
注意:
此status是个wait的参数指向的整型变量。
int main(int argc, char *argv[])
{
pid_t pid;
pid=fork();
if(pid<0)
{
perror("fail to fork");
return -1;
}
if(pid == 0)
{
int i = 0;
for(i=0;i<5;i++)
{
printf("this is son process\n");
sleep(1);
}
//使用exit退出当前进程并设置退出状态
exit(2);
}
else
{
//使用wait在父进程中阻塞等待子进程的退出
//不接收子进程的退出状态
//wait(NULL);
//接收子进程的退出状态,子进程中必须使用exit或者_exit函数退出进程是发送退出状态
int status = 0;
wait(&status);
if(WIFEXITED(status) != 0)
{
printf("The son process return status: %d\n", WEXITSTATUS(status));
}
printf("this is father process\n");
}
return 0;
}
2、waitpid函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status,int options)
功能:等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
参数:
pid:指定的进程或者进程组
pid>0:等待进程ID等于pid的子进程。
pid=0:等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会等待它。
pid=‐1:等待任一子进程,此时waitpid和wait作用一样。
pid<‐1:等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值
status:保存子进程退出时的状态信息
options:选项
0:同wait,阻塞父进程,等待子进程退出。
WNOHANG:没有任何已经结束的子进程,则立即返回。
WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(跟踪调试,很少用到)
返回值:
成功:返回状态改变了的子进程的进程号;如果设置了选项WNOHANG并且pid指定的进程存在则返回0。
失败:返回‐1。
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD。
wait(status) <==> waitpid(‐1, status, 0)
int main(int argc, char *argv[])
{
pid_t pid;
pid=fork();
if(pid < 0)
{
perror("fail to fork");
return -1;
}
if(pid == 0)
{
int i = 0;
for(i=0;i<5;i++)
{
printf("this is son process\n");
sleep(1);
}
exit(0);
}
else
{
waitpid(pid, NULL, 0);
printf("this is father process\n");
}
return 0;
}
9、孤儿进程
在一个启动的进程中创建子进程,这时候父子进程同时运行,但是父进程由于某种原因先退出了,子进程还在运行,这时候这个子进程就可以被称之为孤儿进程(跟现实是一样的)。
操作系统是非常关爱运行的每一个进程的,当检测到某一个进程变成了孤儿进程,这时候系统中就会有一个固定的进程领养这个孤儿进程(有干爹了)。如果使用 Linux 没有桌面终端,这个领养孤儿进程的进程就是 init 进程(PID=1),如果有桌面终端,这个领养孤儿进程就是桌面进程。
那么问题来了,系统为什么要领养这个孤儿进程呢?
在子进程退出的时候, 进程中的用户区可以自己释放, 但是进程内核区的pcb资源自己无法释放,必须要由父进程来释放子进程的pcb资源,孤儿进程被领养之后,这件事儿干爹就可以代劳了,这样可以避免系统资源的浪费。
下面这段代码就可以得到一个孤儿进程:
int main()
{
// 创建子进程
pid_t pid = fork();
// 父进程
if(pid > 0)
{
printf("我是父进程, pid=%d\n", getpid());
}
else if(pid == 0)
{
sleep(1); // 强迫子进程睡眠1s, 这个期间, 父进程退出, 当前进程变成了孤儿进程
// 子进程
printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
}
return 0;
10、僵尸进程
在一个启动的进程中创建子进程,这时候就有了父子两个进程,父进程正常运行,子进程先与父进程结束,子进程无法释放自己的 PCB 资源,需要父进程来做这个件事儿,但是如果父进程也不管,这时候子进程就变成了僵尸进程。
僵尸进程不能将它看成是一个正常的进程,这个进程已经死亡了,用户区资源已经被释放了,只是还占用着一些内核资源(PCB)。
僵尸进程的出现是由于这个已死亡的进程的父进程不作为造成的。
运行下面的代码就可以得到一个僵尸进程了:
int main()
{
pid_t pid;
// 创建子进程
for(int i=0; i<5; ++i)
{
pid = fork();
if(pid == 0)
{
break;
}
}
// 父进程
if(pid > 0)
{
// 需要保证父进程一直在运行
// 一直运行不退出, 并且也做回收, 就会出现僵尸进程
while(1)
{
printf("我是父进程, pid=%d\n", getpid());
sleep(1);
}
}
else if(pid == 0)
{
// 子进程, 执行这句代码之后, 子进程退出了
printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
}
return 0;
}
消灭僵尸进程的方法是,杀死这个僵尸进程的父进程,这样僵尸进程的资源就被系统回收了。
通过 kill -9 僵尸进程PID 的方式是不能消灭僵尸进程的,这个命令只对活着的进程有效,僵尸进程已经死了。