简 述: 本篇主要讲解如下知识点:
- 并行和并发的区别
- 进程控制块
PCB
和进程的五种状态 - 使用
fork()
创建子进程,创建多个兄弟子进程(不含孙进程) - 验证进程之间没有共享全局变量
exec()
函数族的使用:execl()
和execlp()
[TOC]
本文初发于 “偕臧的小站“,同步转载于此。
编程环境:
💻: uos20
📎 gcc/g++ 8.3
📎 gdb8.0
💻: MacOS 10.14
📎 gcc/g++ 9.2
📎 gdb8.3
并行和并发:
并发:一个时间段,处理请求的个数
eg:在一个时间段内, 处理的请求个数。 只有一个 cpu 对任务进行处理。 (每个人都吃一口饭,但是所有人都没有吃饱,短时间内让大量的快速吃,然后这样轮循环。)
并行:多个进程同时进行任务分配:
可以看做,有多个 cpu 对多个任务进行同时处理。
PCB 和 进程的五种状态:
进程控制块 PCB:
此部分了解即可: 每一个进程在内核中都有一个进程控制块 PCB 来维护进程相关的信息,其是有一个 400 多行的结构体组成,其中主要的需要了解的部分如下:
- 进程 id。系统中每一个进程有唯一的一个 id,在 C 语言中,用 pid_t 类型表示。(凡是 xxx_t 一般都是 #define 重新定一个类型, 这里实际就上课非负整数)
- 进程的状态,有就绪,运行,挂起,停止 等状态。(还有一个初始态 )
- 进程切换时候,需要保存和恢复一些的 CPU 寄存器
- 描述虚拟地址空间的信息(每启动一个进程,就对应一个虚拟地址空间)
- 描述控制终端的信息
- 当前的工作目录
- umask 掩码(执行 umask)
- 文件描述符表,包含很多执行 file 结构体的指针(一个进程最多打开 1024 个)
- 和信号相关的信息
- 用户 uid 和组 gid (用stat xxx 可以查看)
- 会话(Session)和进程组。(会话就是多个进程组)
- 进程可以使用的资源上限(Resource Limit)。(使用
ulimit -a
可看)
进程的五种状态:
看着如下的这张图,想起里了我之前初看的计算机操作系统的相关知识的时候,感慨颇多。
- 状态切换的关键:
- 获得 cpu
- cpu 执行
进程控制块,父进程创建子进程的分析 🎃:
父子进程空间,其虚拟地址空间是完全一样的(用户区的所有数据都是一样的,也是会运行的相同的一段代码;系统区也是一样的,唯一不一样的就是系统区的 id 号)。
创建一个子进程,是使用 int fork(void)
函数来创建,在执行完这个 fork 函数之后,将用户区的内容全部拷贝(包括接下来要执行的代码)过去。且该 fork 函数有两个返回值,执行完 fork() 后,父进程会返回子进程的 id(大于 0),子进程会返回一个 0,后面执行相同的代码,依靠这个来区分父子进程。 下面写一个代码片来表明:
对照上面的两个图,可以思考如下问题:
- fork 之后,函数的返回值?
- fork() 返回值 > 0,就是父进程;fork() 返回值 == 0,就是子进程
- 子进程创建成功之后,代码的执行位置?
- 父进程执行哪里(才创建子进程),子进程就重哪里里开始执行。
- 父子进程的执行顺序?
- 不一定,自己去抢占 cpu 资源。执行顺序是程序员无法控制的。(cpu 轮循执行)
- 如何区分父子进程?
- 通过 fork() 返回值
- 如何获取当前进程和父进程的 id?
- 获取进程 id:获取当前进程的 PID:
getpid()
;获取当前进程的父进程的 PID:getppid()
- 获取进程 id:获取当前进程的 PID:
对与上面的问题,写如下代码片段进程测试,main.cpp 里面的代码是:代码下载 main.cpp
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
for (int i = 0; i < 4; ++i) {
printf("++++i = %d\n", i);
}
pid_t pid = -1;
pid = fork();
if (pid > 0)
printf("this is a parent process, pid = %d\n", getpid());
else if (pid == 0)
printf("this is a child process, pid = %d ppid = %d\n", getpid(), getppid());
for (int i = 0; i < 4; ++i) {
printf("----i = %d\n", i);
}
return 0;
}
其运行结果如下:
父进程如何创建多个兄弟子进程(不含孙进程)🎃:
看了上面的小的例子之后,发现这样可以创建子进程;进一步,怎么创建多个子进程,但是不要孙创建进程?有怎么判断子进程的顺序编号?怎么得到最开始父进程?
代码如下: 代码下载 childProcess.cpp
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t pid = -1;
int i = 0;
for (i = 0; i < 3; ++i) {
pid = fork();
if (pid == 0) //如果当前进程是子进程,就跳出此循环,不在进行 fork() 创建新的子进程
break;
}
if (i == 0)
printf("this is a child process (i == 0), pid = %d ppid = %d\n", getpid(), getppid());
if (i == 1)
printf("this is a child process (i == 1), pid = %d ppid = %d\n", getpid(), getppid());
if (i == 2)
printf("this is a child process (i == 2), pid = %d ppid = %d\n", getpid(), getppid());
if (i == 3)
printf("this is a parent process (i == 3), pid = %d ppid = %d\n", getpid(), getppid());
return 0;
}
分析代码片:
若是没有上面的 13、14 行代码,那么创建的就不止 3 个子进程;就是如下图片所示的结果; 使用 fork() 创建子进程的时候,是进行虚拟地址控件的拷贝(可在草稿纸上面画图理解,是一个地址块所有的代码和变量值都复制一份),他们的当时 i 是 1 的时候,创建的新的子进程的 i 也是 1,不会是初值 0;同理,当 i 是 2 时候创建新进程,其新进程的 i 值是 2,而不是 1; 但是禁止子进程创建孙进程,那么就可以使用 for() 循环生成多个兄弟子进程,且没有任何的孙进程。
运行结果:
可以自己每运行行代码,标记一下 i 的值,以及此 i 的值时刻复制当前进程的虚拟地址空间,给新的子进程;所以 可以通过编号 i 的数值,来判断兄弟子进程的先后顺序;最后编号最大的那个,就是最初的父进程。
进程之间是否共享全局变量:🎃
读时共享(同一个物理内存的上面区域,内存地址为同一个变量)
写的时候复制
父子进程之间能否使用全局变量通信?
- 不能。因为两个进程之间内存不共享(读时共享,写时复制)
代码验证,改写上面的代码,添加一个全局变量 int g_num = 200,进行验证:
#include <stdio.h> #include <unistd.h> int g_num = 200; int main(int argc, char *argv[]) { pid_t pid = -1; int i = 0; for (i = 0; i < 3; ++i) { pid = fork(); if (pid == 0) //如果当前进程是子进程,就跳出此循环,不在进行 fork() 创建新的子进程 break; } if (i == 0) { g_num += 5; printf("this is a child process (i == 2), pid = %d ppid = %d\n", getpid(), getppid()); } if (i == 1){ g_num += 5; printf("this is a child process (i == 2), pid = %d ppid = %d\n", getpid(), getppid()); } if (i == 2) { g_num += 5; printf("this is a child process (i == 2), pid = %d ppid = %d\n", getpid(), getppid()); } if (i == 3) { g_num += 100; printf("this is a parent process (i == 3), pid = %d ppid = %d\n", getpid(), getppid()); } printf("g_nmu = %d\n", g_num); return 0; }
运行截图:
草稿图分析:
显示当前进程的状态 ps:
ps aus
显示不依赖于终端的进程(终端是用来和用户进行交互的)ps aux | grep 查找的进程名
使用管道查找指定的进程名称ps ajx
显示更多的进程信息; PID、PPID、PGID、SID(进程,父进程,组进程,会话)(会话就是多个组进程)ps ajx | grep 查找的进程名
若是没有找到,也会有一行显示信息,显示的是 grep 进程相关的信息
删除执行中的程序或工作 kill:
Linux kill命令用于删除执行中的程序或工作。
kill可将指定的信息送至程序。预设的信息为SIGTERM(15),可将指定程序终止。若仍无法终止该程序,可使用SIGKILL(9)信息尝试强制删除程序。程序或工作的编号可利用ps指令或jobs指令查看。
kill -9 将被杀死的进程
-9 为 杀死信号kill -l
显示所有,1-31 通常使用;32 和 33 没有; 34-51 号通常为系统预留
使用 kill 命令之后,有一点困惑,使用 kill -l
命令之后;
在 Ubuntu 18.4 显示如下, 和 MacOS 显示有区别, 有清楚的,可以在下面留言帮我解答一下也可以哦。
只有第9种信号(SIGKILL)才可以无条件终止进程,其他信号进程都有权利忽略。 下面是常用的信号:
HUP 1 终端断线 INT 2 中断(同 Ctrl + C) QUIT 3 退出(同 Ctrl + \) KILL 9 强制终止 TERM 15 终止 CONT 18 继续(与STOP相反, fg/bg命令) STOP 19 暂停(同 Ctrl + Z)
exec 函数族 🎃:
作用: 让父子进程执行不相干的操作;能够替换进程空间中源代码的 .txt
段;当前程序中调用另外一个应用程序 (调用 exec 之前,需要 fork );
exec() 函数族, 也不要判断返回值:若是函数执行成功,不返回(.text 的代码立即被全部替换,后面的代码也不会执行);若是执行失败,可以调用 perror(“xx 提示:”) 来打印错误信息,退出当前进程;
执行指定目录下的程序 execl():
int execl(const char *path, const char *arg, ... /*, (char *)0 */);
path: 需要执行的程序的绝对路径(推荐绝对路经)
变参 arg: 要执行的程序的需要的参数
- 第一个 arg: 占位(内容随便写什么,但不可为空)
- 后边的 arg: 命令的参数
参数写完之后: NULL (作用是哨兵,表示该命令的参数已经输入完毕)
一般是执行自己写的程序(也可以执行系统自带的命令)
写一个例子验证一下:代码下载
- 代码如下:
#include <stdio.h> #include <unistd.h> int main(int argc, char *argv[]) { for (int i = 0; i < 3; i++) printf("-----i = %d\n", i); pid_t pid = fork(); //创建子进程 if (pid == 0) execl("/bin/ls", "占位参数", "-al", NULL); //ls 程序使用子进程的地址空间 for (int i = 0; i < 3; i++) //这段打印只会(父进程)被执行一遍。 printf("+++++i = %d\n", i); return 0; }
过程分析:
第一段打印”—–i =” 内容,是只会被父进程执行一遍的,子进程不会执行该段代码(分析见上);结束时候打印 “+++++i =” 内容时候,是只会执行一遍的,还是被父进程执行的,而子进程不执行 的原因是,当第 12 行的代码execl(“ls”) 执行结束之后,其子进程的虚拟地址空间的以用户区域的 .text 代码段的二进制代码被替换为 /bin/ls 这个程序的代码;其后面面也都是运行 ls 程序的代码内容。
运行结果:
执行 PATH 目录下的程序 execlp():
int execlp(const char *file, const char *arg, ... /*, (char *)0 */);
- file: 需要执行的程序的名字
- 变参 arg: 要执行的程序的需要的参数
- 第一个 arg: 占位(内容随便写什么,但不可为空)
- 后边的 arg: 命令的参数
- 参数写完之后: NULL (作用是哨兵,表示该命令的参数已经输入完毕)
- 一般是执行系统自带的命令(也就是 /bin 下的程序)
- execlp 执行自定义程序的程序: file 参数绝对路径
下载/系列地址:
https://github.com/xmuli/linuxExample
欢迎 star 和 fork 这个系列的 linux 学习,附学习进阶的路线图。