文章目录
- 前言
- 一、程序和进程
- 二、并行和并发
- 三、进程控制块PCB
- 进程状态
- 四、创建进程相关函数
- 1、fork函数
- fork函数总结
- 2、getpid/getppid
- 3、exec函数族
- 3.1、execl函数
- 3.2、execlp函数
- 3.3、exec函数族原理介绍
- 五、进程回收
- 1、为什么要进行进程资源回收
- 2、孤儿进程
- 3、僵尸进程
- 4、进程回收函数
- 4.1、wait函数
- 4.2、waitpid函数
前言
本文用于Linux系统编程中进程相关知识。
一、程序和进程
程序,是指编译好的二进制文件,在磁盘上,占用磁盘空间, 是一个静态的概念.
进程,一个启动的程序, 进程占用的是系统资源,如:物理内存,CPU,终端等,是一个动态的概念。程序 → 剧本(纸) ,进程 → 戏(舞台、演员、灯光、道具…)
同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)。
二、并行和并发
并发指的是在一个时间段内,在同一个CPU上同时运行多个程序,而并行是指两个或两个以上的程序在同一时刻发生(需要多颗CPU)。
三、进程控制块PCB
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
/usr/src/linux-headers-4.4.0-96/include/linux/sched.h
文件的1390行处可以查看struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:
-
进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
-
进程的状态,有初始、就绪、运行、挂起、停止等状态。
-
进程切换时需要保存和恢复的一些CPU寄存器。
-
描述虚拟地址空间的信息。
-
描述控制终端的信息。
-
当前工作目录(Current Working Directory)。
getcwd --pwd
-
umask掩码。
-
文件描述符表,包含很多指向file结构体的指针。
-
和信号相关的信息。
-
用户id和组id。
-
会话(Session)和进程组。
-
进程可以使用的资源上限(Resource Limit)。
ulimit -a
进程状态
进程有5种状态:初始、就绪、运行、挂起与终止。其中初始态为进程准备阶段,常与就绪态结合来看。
四、创建进程相关函数
1、fork函数
函数作用:创建子进程
函数原型:pid_t fork(void);
函数参数:无
返回值:
成功:父进程返回子进程的PID,子进程返回0
失败:返回-1,设置errno值
fork函数总结
fork函数的返回值?
父进程返回子进程的PID,是一个大于0数;
子进程返回0;
特别需要注意的是:不是fork函数在一个进程中返回2个值,而是在父子进程各自返回一个值。
子进程创建成功后,代码的执行位置?
父进程执行到什么位置,子进程就从哪里执行
如何区分父子进程
通过fork函数的返回值
父子进程的执行顺序
不一定,哪个进程先抢到CPU,哪个进程就先执行
2、getpid/getppid
函数原型:pid_t getpid(void);
函数作用:得到当前进程的pid
函数原型:pid_t getppid(void);
函数作用:得到当前进程父进程的PID
3、exec函数族
有的时候需要在一个进程里面执行其他的命令或者是用户自定义的应用程序,此时就用到了exec函数族当中的函数。
使用方法一般都是在父进程里面调用fork创建处子进程,然后在子进程里面调用exec函数。
3.1、execl函数
函数原型: int execl(const char *path, const char *arg, ... /* (char *) NULL */);
参数介绍:
path: 要执行的程序的绝对路径
arg:占位,通常写应用程序的名字
arg后面的: 命令的参数
参数写完之后: NULL
返回值:
成功,则不返回,不会再执行exec函数后面的代码;
失败,会执行execl后面的代码,可以用perror打印错误原因。
execl函数一般执行自己写的程序。
3.2、execlp函数
函数原型: int execlp(const char *file, const char *arg, .../* (char *) NULL */);
参数介绍:
file: 执行命令的名字, 根据PATH环境变量来搜索该命令
arg:占位
arg后面的: 命令的参数
参数写完之后: NULL
返回值:
成功,则不返回,不会再执行exec函数后面的代码;
失败,会执行exec后面的代码,可以用perror打印错误原因。
execlp函数一般是执行系统自带的程序或者是命令.
3.3、exec函数族原理介绍
exec族函数的实现原理图:
如:execlp(“ls”, “ls”, “-l”, NULL);
总结:
exec函数是用一个新程序替换了当前进程的代码段、数据段、堆和栈;原有的进程空间没有发生变化,并没有创建新的进程,进程PID没有发生变化。
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建子进程
pid_t pid = fork();
if(pid<0) //fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid>0)//父进程
{
printf("father: [%d], pid==[%d], fpid==[%d]\n", pid, getpid(),getppid());
//sleep(1);
}
else if(pid==0) //子进程
{
printf("child: pid==[%d], fpid==[%d]\n", getpid(), getppid());
//execl("/bin/ls", "ls", "-l", NULL);
//execl("./test", "test", "hello", "world", "ni", "hao", NULL);
execlp("ls", "ls", "-l", NULL);
perror("execl error");
}
return 0;
}
五、进程回收
1、为什么要进行进程资源回收
当一个进程退出之后,进程能够回收自己的用户区的资源,但是不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源的浪费。
2、孤儿进程
若子进程的父进程已经死掉了,而子进程还存活着,这个进程就成了孤儿进程。为了保证每个进程都有一个父进程,孤儿进程会被init进程领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。
3、僵尸进程
若子进程死了,父进程还活着,但是父进程没有调用wait或waitpid函数完成对子进程的回收,则该子进程就成了僵尸进程。
如何解决僵尸进程
由于僵尸进程是一个已经死亡的进程,所以不能使用kill命令将其杀死通过杀死其父进程的方法可以消除僵尸进程杀死其父进程后,这个僵尸进程会被init进程领养,由init进程完成对僵尸进程的回收。
4、进程回收函数
4.1、wait函数
函数原型:pid_t wait(int *status);
函数作用:
1、阻塞并等待子进程退出
2、回收子进程残留资源
3、获取子进程结束状态(退出原因)
返回值:
成功:清理掉的子进程ID;
失败:-1 (没有子进程)
status参数:子进程的退出状态 -- 传出参数
WIFEXITED(status):为非0 → 进程正常结束
WEXITSTATUS(status):获取进程退出状态
WIFSIGNALED(status):为非0 → 进程异常终止
WTERMSIG(status):取得进程终止的信号编号。
4.2、waitpid函数
函数原型:pid_t waitpid(pid_t pid, int *status, in options);
函数作用:同wait函数
函数参数:
pid:
pid = -1 等待任一子进程。与wait等效。
pid > 0 等待其进程ID与pid相等的子进程。
pid = 0 等待进程组ID与目前进程相同的任何子进程,也就是说任何和调用
waitpid()函数的进程在同一个进程组的进程。
pid < -1 等待其组ID等于pid的绝对值的任一子进程。(适用于子进程在其他组的情况)
status: 子进程的退出状态,用法同wait函数。
options:设置为WNOHANG,函数非阻塞,设置为0,函数阻塞。
函数返回值
>0:返回回收掉的子进程ID;
-1:无子进程
=0:参3为WNOHANG,且子进程正在运行。
//父进程调用wait函数完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
//创建子进程
pid_t pid = fork();
if(pid<0) //fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid>0)//父进程
{
printf("father: [%d], pid==[%d], fpid==[%d]\n", pid, getpid(),getppid());
int status;
pid_t wpid = wait(&status);
printf("wpid==[%d]\n", wpid);
if(WIFEXITED(status)) //正常退出
{
printf("child normal exit, status==[%d]\n", WEXITSTATUS(status));
}
else if(WIFSIGNALED(status)) //被信号杀死
{
printf("child killed by signal, signo==[%d]\n", WTERMSIG(status));
}
}
else if(pid==0) //子进程
{
printf("child: pid==[%d], fpid==[%d]\n", getpid(), getppid());
sleep(20);
return 9;
}
return 0;
}