目录
1进程相关概念
1.1程序和进程
- 程序,是指编译好的二进制文件,在磁盘上,占用磁盘空间, 是一个静态的概念.
- 进程,一个启动的程序, 进程占用的是系统资源,如:物理内存,CPU,终端等,是一个动态的概念
- 程序 → 剧本(纸)
- 进程 → 戏(舞台、演员、灯光、道具...)
同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)
每启动一个程序都会有一个进程PID,即使是相同的程序多次启动也会有不同个PID。
1.2并行和并发
并发,在一个时间段内, 是在同一个cpu上, 同时运行多个程序。一个时间片内只有一个程序。
如:若将CPU的1S的时间分成1000个时间片,每个进程执行完一个时间片必须无条件让出CPU的使用权,这样1S中就可以执行1000个进程。
并行性指两个或两个以上的程序在同一时刻发生(需要有多颗)。单核cpu不能实现,一个CPU内有多个程序。
1.3PCB-进程控制块
每个进程在内核中都有一个进程控制块(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
1.4进程状态(面试考)
- 进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
- 程序有一个瞬时的状态,初始态,很快就会到就绪态
- 挂起状态不能直接回到运行状态,应该先回到就绪状态,然后再回到运行状态
2 创建进程
2.1 fork函数
函数作用:创建子进程
- 原型: pid_t fork(void);
函数参数:无
返回值:调用成功:父进程返回子进程的PID,子进程返回0;
调用失败:返回-1,设置errno值。
- fork函数代码片段实例
调用fork函数的内核实现原理:
fork函数总结
►fork函数的返回值?
父进程返回子进程的PID,是一个大于0数;
子进程返回0;
特别需要注意的是:不是fork函数在一个进程中返回2个值,而是在父子进程各自返回一个值。
►子进程创建成功后,代码的执行位置?
父进程执行到什么位置,子进程就从哪里执行
►如何区分父子进程
通过fork函数的返回值
►父子进程的执行顺序
不一定,哪个进程先抢到CPU,哪个进程就先执行
2.2 ps命令和kill命令
ps aux | grep "xxx"
ps ajx | grep "xxx"
- -a:(all)当前系统所有用户的进程
- -u:查看进程所有者及其他一些信息
- -x:显示没有控制终端的进程 -- 不能与用户进行交互的进程【输入、输出】
- -j: 列出与作业控制相关的信息
- kill -l 查看系统有哪些信号
- kill -9 pid 杀死某个线程
2.3 getpid/getppid
getpid - 得到当前进程的PID
pid_t getpid(void);
getppid - 得到当前进程的父进程的PID
pid_t getppid(void);
2.4练习题
编写程序,循环创建多个子进程,要求如下:
-
多个子进程是兄弟关系。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
//循环创建子进程
int main()
{
int i = 0;
for(; i < 3; i++)
{
// pid_t fork(void);
pid_t pid = fork();
//创建失败情况 如果进程太多可能会出现失败的情况
if (pid < 0)
{
perror("fork error!");
}
else if (pid > 0)
{
printf("父进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
else if (pid == 0) //子进程 getpid()返回自身的pid
{
printf("子进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
//如果不break 子进程也会调用fork函数
break;
}
}
printf("第%d子进程 pid == [%d] parent == [%d]\n", i, getpid(), getppid());
sleep(10);
return 0;
}
2.判断子进程是第几个子进程
画图讲解创建多个子进程遇到的问题
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// pid_t fork(void);
pid_t pid = fork();
//创建失败情况 如果进程太多可能会出现失败的情况
if (pid < 0)
{
perror("fork error!");
}
else if (pid > 0) //父进程 getpid()返回自身的pid getppid()返回父亲节点
{
printf("父进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
else if (pid == 0) //子进程 getpid()返回自身的pid
{
//此处sleep是为了避免父进程还没有执行完,子进程已经结束了
sleep(1);
printf("子进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
sleep(10);
return 0;
}
- 编写程序,测试父子进程是否能够共享全局变量 不能共享
重点通过这个案例讲解读时共享,写时复制
如果都只是对全局变量进行读操作,那么是共享的,如果有一方进行了修改,
那么系统会先复制出来一份值,然后修改后再映射回去。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int globeVar = 99;
int main()
{
// pid_t fork(void);
pid_t pid = fork();
//创建失败情况 如果进程太多可能会出现失败的情况
if (pid < 0)
{
perror("fork error!");
}
else if (pid > 0) //父进程 getpid()返回自身的pid getppid()返回父亲节点
{
printf("父进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
globeVar++;
}
else if (pid == 0) //子进程 getpid()返回自身的pid
{
//此处sleep是为了避免父进程还没有执行完,子进程已经结束了
sleep(1);
printf("子进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
printf("globeVar == %d", globeVar);
}
sleep(10);
return 0;
}
3 exec函数族
3.1 函数作用和函数介绍
有的时候需要在一个进程里面执行其他的命令或者是用户自定义的应用程序,此时就用到了exec函数族当中的函数。
使用方法一般都是在父进程里面调用fork创建处子进程,然后在子进程里面调用exec函数。
execl函数
函数原型: int execl(const char *path, const char *arg, ... /* (char *) NULL */);
参数介绍:
- path: 要执行的程序的绝对路径
- 变参arg: 要执行的程序的需要的参数
- arg:占位,通常写应用程序的名字
- arg后面的: 命令的参数
- 参数写完之后: NULL
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行execl后面的代码,可以用perror打印错误原因。
execl函数一般执行自己写的程序。
execlp函数
函数原型: int execlp(const char *file, const char *arg, .../* (char *) NULL */);
参数介绍:
- file: 执行命令的名字, 根据PATH环境变量来搜索该命令
- arg:占位
- arg后面的: 命令的参数
- 参数写完之后: NULL
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行exec后面的代码,可以用perror打印错误原因。
execlp函数一般是执行系统自带的程序或者是命令.
3.2 exec函数族原理介绍
exec族函数的实现原理图:
如:execlp(“ls”, “ls”, “-l”, NULL);
子进程的代码段会被ls替换掉
总结:
exec函数是用一个新程序替换了当前进程的代码段、数据段、堆和栈;原有的进程空间没有发生变化,并没有创建新的进程,进程PID没有发生变化。
exec一般用于执行用户自定义的应用程序
execlp一般指向系统命令
3.3 exec函数练习
使用execl函数执行一个用户自定义的应用程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// pid_t fork(void);
pid_t pid = fork();
if (pid < 0)
{
perror("fork error!");
}
else if (pid > 0) //父进程 getpid()返回自身的pid getppid()返回父亲节点
{
printf("父进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
else if (pid == 0) //子进程 getpid()返回自身的pid
{
//通过子进程调用execl执行命令
//第一个参数是路径 第二个参数是命令 第三个参数为命令行参数
execl("/usr/bin/ls", "ls", "-l", NULL);
//调用自己写的程序 第一个参数路径 第二个参数程序名 第三个参数第四个参数....传入到函数的参数
execl("./test", "test", "hello", "world!", NULL);
printf("子进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
return 0;
}
使用execlp函数执行一个linux系统命令
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// pid_t fork(void);
pid_t pid = fork();
if (pid < 0)
{
perror("fork error!");
}
else if (pid > 0) //父进程 getpid()返回自身的pid getppid()返回父亲节点
{
printf("父进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
else if (pid == 0) //子进程 getpid()返回自身的pid
{
//execlp使用使用相对路径
execlp("ls", "ls", "-l", NULL);
printf("子进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
return 0;
}
注意:当execl和execlp函数执行成功后,不返回,并且不会执行execl后面的代码逻辑,原因是调用execl函数成功以后,exec函数指定的代码段已经将原有的代码段替换了。
4进程回收
4.1 为什么要进行进程资源的回收
当一个进程退出之后,进程能够回收自己的用户区的资源,但是不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源的浪费。
4.2 孤儿进程
孤儿进程的概念:
若子进程的父进程已经死掉,而子进程还存活着,这个进程就成了孤儿进程。
- 为了保证每个进程都有一个父进程,孤儿进程会被init进程领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。
- 模拟孤儿进程的案例
编写模拟孤儿进程的代码讲解孤儿进程,验证孤儿进程的父进程是否由原来的父进程变成了init进程。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// pid_t fork(void);
pid_t pid = fork();
if (pid < 0)
{
perror("fork error!");
}
else if (pid > 0) //父进程 getpid()返回自身的pid getppid()返回父亲节点
{
printf("父进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
else if (pid == 0) //子进程 getpid()返回自身的pid
{
//此处sleep是为了避免父进程还没有执行完,子进程已经结束了
sleep(1);
printf("子进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
sleep(5);
printf("子进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
sleep(1);
return 0;
}
4.3 僵尸进程
僵尸进程的概念:
若子进程死了,父进程还活着, 但是父进程没有调用wait或waitpid函数完成对子进程的回收,则该子进程就成了僵尸进程。
需要在另外一个终端查看进程的运行状态。
如何解决僵尸进程
- 由于僵尸进程是一个已经死亡的进程,所以不能使用kill命令将其杀死
- 通过杀死其父进程的方法可以消除僵尸进程。
杀死其父进程后,这个僵尸进程会被init进程领养,由init进程完成对僵尸进程的回收。
模拟僵尸进程的案例
编写模拟僵尸进程的代码讲解僵尸进程, 验证若子进程先于父进程退出, 而父进程没有调用wait或者waitpid函数进行回收, 从而使子进程成为了僵尸进程.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// pid_t fork(void);
pid_t pid = fork();
if (pid < 0)
{
perror("fork error!");
}
else if (pid > 0) //父进程 getpid()返回自身的pid getppid()返回父亲节点
{
sleep(20);
printf("父进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
else if (pid == 0) //子进程 getpid()返回自身的pid
{
printf("子进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
}
return 0;
}
4.4 进程回收函数
-
wait函数
- 函数原型:
pid_t wait(int *status);
- 函数作用:
- 阻塞并等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)。
- 返回值:
- 成功:清理掉的子进程ID;
- 失败:-1 (没有子进程)
- status参数:子进程的退出状态 -- 传出参数
- WIFEXITED(status):为非0 → 进程正常结束
WEXITSTATUS(status):获取进程退出状态
- WIFSIGNALED(status):为非0 → 进程异常终止
WTERMSIG(status):取得进程终止的信号编号。
wait函数练习
使用wait函数完成父进程对子进程的回收
//父进程调用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;
}
-
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,且子进程正在运行。
waitpid函数练习
使用waitpid函数完成对子进程的回收
//父进程调用waitpid函数完成对子进程的回收
#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 = waitpid(pid, &status, 0); // == pid_t wpid = wait(&status);
//保证父进程在子进程的后面进行释放
while (1)
{
//-1代表等待任意子进程 WNOHANG:不阻塞
pid_t wpid = waitpid(-1, &status, WNOHANG);
if(wpid > 0)//返回的是回收的子进程
{
printf("wpid==[%d]\n", wpid);
if(WIFEXITED(status)) //正常退出 man 2 waitpid可查询参数
{
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(wpid == 0)//代表子进程还活着
{
//printf("child is living, wpid = [%d]\n", wpid);
}
else if (wpid == -1)
{
printf("no child is living, wpid = [%d]\n", wpid);
break;
}
}
}
else if(pid==0) //子进程
{
printf("child: pid==[%d], fpid==[%d]\n", getpid(), getppid());
sleep(2);
return 9;
}
return 0;
}
5 作业
5.1测试父子进程之间是否共享文件
可以共享文件。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("./test.log", O_RDWR | O_CREAT, 0777);
if (fd < 0)
{
//提示出错信息
perror("OPEN error");
return -1;
}
// pid_t fork(void);
pid_t pid = fork();
//创建失败情况 如果进程太多可能会出现失败的情况
if (pid < 0)
{
perror("fork error!");
}
else if (pid > 0) //父进程 getpid()返回自身的pid getppid()返回父亲节点
{
printf("父进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
write(fd, "hello wrold", strlen("hello world"));
close(fd);
}
else if(pid == 0)
{
printf("子进程 pid == [%d] parent == [%d]\n", getpid(), getppid());
char buff[255];
int n;
sleep(1); //先写入数据再读取数据
lseek(fd, 0, SEEK_SET);//移动到表头
n = read(fd, buff, sizeof(buff));
printf("read over : n = [%d], buff = [%s]", n, buff);
close(fd);
}
return 0;
}
5.2使用Fork创建三个进程
父进程fork三个子进程:
其中一个调用ps命令;
一个调用自定义应用程序;
一个调用会出现段错误的程序。
父进程回收三个子进程(waitpid),并且打印三个子进程的退出状态。
=== 段错误 ===
1>. 访问了非法内存
2>. 访问了不可写的区域进行写操作
3>. 栈空间溢出
char* p = “hello,world”
p【0】=‘a’;
//调用fork函数创建子进程, 并完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int i = 0;
int n = 3;
for(i=0; i<n; i++)
{
//fork子进程
pid_t pid = fork();
if(pid<0) //fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid>0) //父进程
{
printf("father: fpid==[%d], cpid==[%d]\n", getpid(), pid);
sleep(1);
}
else if(pid==0) //子进程
{
printf("child: fpid==[%d], cpid==[%d]\n", getppid(), getpid());
break;
}
}
//父进程
if(i==3)
{
printf("[%d]:father: fpid==[%d]\n", i, getpid());
pid_t wpid;
int status;
while(1)
{
wpid = waitpid(-1, &status, WNOHANG);
if(wpid==0) // 没有子进程退出
{
continue;
}
else if(wpid==-1) //已经没有子进程了
{
printf("no child is living, wpid==[%d]\n", wpid);
exit(0);
}
else if(wpid>0) //有子进程退出
{
if(WIFEXITED(status))
{
printf("normal exit, status==[%d]\n", WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))
{
printf("killed by signo==[%d]\n", WTERMSIG(status));
}
}
}
}
//第1个子进程
if(i==0)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
execlp("ls", "ls", "-l", NULL);
perror("execl error");
exit(-1);
}
//第2个子进程
if(i==1)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
execl("/home/itcast/test/course/day6/0527/zuoye/hello", "hello", "1111", "2222", NULL);
perror("execl error");
return -1;
}
//第3个子进程
if(i==2)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
execl("/home/itcast/test/course/day6/0527/zuoye/test", "test", NULL);
perror("execl error");
return -1;
}
return 0;
}