程序和进程
程序是二进制文件,在磁盘上,不占用系统资源(CPU,内存,打开的文件,设备,锁….)
进程,比较抽象的概念,活跃的程序,占用系统资源
可以通俗理解为:程序是剧本,进程是上演的戏剧
需要知道的前置知识
并发
单道程序设计:DOS系统,程序只能排队占用CPU执行,
多道程序设计:想要实现多开进程,而CPU如果只有一个,需要划分时间分片设置一个时间中断,硬件手段,给打开的多个程序时间中断,给进程们进行分配CPU的使用时限,由于CPU计算相当快,人眼看不到停止轮次,就会认为是并行运行,实际上是并发
CPU和MMU
CPU:硅,去沙滩抓把沙子做一做就弄明白了
请移步机组文章
这里单独拿出MMU
MMU基本工作原理
MMU是一个内存管理单元,在CPU中
虚拟内存只是个概念,需要MMU将虚拟内存的虚拟地址映射在对应的物理地址上。除了映射功能,还能设置内存访问的级别,一般英特尔cpu设定的内存访问级别都有四个级别
linux只使用了3级与0级两种级别
0级的内核空间,3级就是用户空间
在运行的两个进程,即使程序相同,但是用户区的映射的不是同一块的物理进程,内核区指向的物理内存实在同一块,但是PCB是不一样的,具体还是请移步机组文章
进程控制块
进程描述符(linux系统下的通用称呼)
PCB本质是一个结构体组
结构体成员
- 进程id,系统中每个进程有唯一的id,
- 进程的状态
- 初始化
- 就绪:等待CPU分配时间片
- 运行
- 挂起:等待除了CPU以外的其他资源,主动放弃CPU,可以理解为阻塞状态
- 终止
- 进程切换时需要进行保存的和恢复的一些CPU资源和寄存器的值。
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录
- umask掩码
- 文件描述符表
- 信号相关的信息
- 用户id和组id
- 会话(Session)和进程组
- 进程可以使用的资源上限(Resource Limit)
- 使用ulimit -a命令查看linux中的所有的进程组使用的资源上限
环境变量
linux是什么系统,多用户多任务的开源操作系统
每个用户的配置都不一样,每个人都有不同的启动桌面,不同的默认工具设置,等等,实际上就是环境变量不一样
我们所常见的全局环境变量,用户环境变量可以这么理解
几个常用环境变量
- PATH:二进制的执行文件路径
- SHELL:当前的命令解析器
- LANG:当前的语言
环境变量的打印
1 2 3 4 5 6 7 8 9
| #include <stdio.h> extern char** environ; int main(){ for (int i = 0; environ[i] != NULL; i++) { printf("%s\n", environ[i]); } return 0; }
|
几个环境变量的函数
- setenv()
- getenv()
- clearenv()
使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <stdio.h> #include <stdlib.h> #include <string.h>
int main(){ char* val; const char* name = "ABCDE";
val = getenv(name); printf("1,%s = %s\n", name, val);
setenv(name, "自定义一个环境变量", 1); val = getenv(name); printf("2,%s = %s\n", name, val);
# if 1 int ret = unsetenv("ABCDE"); printf("ret: %d\n", ret); val = getenv(name); printf("3,%s = %s\n", name, val);
#else int ret = unsetenv("ABCDE"); printf("ret: %d\n", ret); val = getenv(name); printf("3,%s = %s\n", name, val);
#endif return 0; }
|
进程控制
fork函数
通过函数fork创建一个子进程
fork函数有两个返回值,一个是是子进程的id,一个是flag(是创建进程成功)
父进程的fork返回的是子进程的id,子进程fork返回的是flag
创建一个子进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <stdio.h> #include <unistd.h> #include <string.h> int main(){
printf("主进程测试"); __pid_t pid; pid = fork(); if (pid == -1) { perror("fork error"); _exit(1); }else if(pid == 0){ printf("我是子进程,pid= %u,我的父进程的 ppid= %u\n", getpid(), getppid()); }else{ printf("我是主进程,pid= %u,我的父进程的 ppid= %u\n", getpid(), getppid()); sleep(1); } printf("测试子进程会不会也执行\n"); return 0; }
|
循环创建五个子进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main(){ int i = 0; __pid_t pid; printf("主进程测试\n");
for (; i < 5; i++) { pid = fork(); if (pid == -1) { perror("fork error"); exit(1); }else if (pid == 0){ break; } } if(i < 5){ sleep(i); printf("我是第%d子进程,pid是:%u, 父进程ppid是:%u\n", i+1, getpid(),getppid()); }else{ sleep(i); printf("主进程结束\n"); } return 0; }
|
以上所有的进程实际上都在同时争夺CPU,需要做进程状态的控制才能保证到底是哪个进程先使用CPU
如果将所有的sleep休眠函数都删除,那么结果其实也是一样的,只是显示上有问题
父子进程的异同
主要是值的相同
相同:
- 全局变量,
- .data
- .text
- 栈
- 堆
- 环境变量
- 用户ID
- 宿主目录
- 进程工作目录
- 信号处理方式等
不同
- 进程ID
- fork返回值
- 父进程ID
- 进程运行时间
- 定时器
- 未决信号集
父子进程间遵循
读时共享,写时复制
共享的
- 文件描述符
- mmap建立的映射区
父子进程的调试
gdb调试默认父进程
在调用子进程函数之前
设置跟踪子进程,就可以走子进程的逻辑
1
| set follow-fork-mode child
|
如果有多个子进程,那就需要设置好条件断点,通过进程的索引值比如你设定的id,或者直接使用pid判断来进入不同的子进程
exec函数族
除了子父进程,子进程还可以执行别的程序
换核不换壳,pid ppid都不变,改变的其实是代码区和等等。
利用的是exec函数族
execlp
exec函数族之一的函数
l 代表的是list(具体可以理解为就是指令参数0~n),而p 表示的是PATH环境变量
利用execlp函数实现子进程执行 环境变量中指定的地址二进制文件ls文件,打印当前的进程的目录下的文件
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ##include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main(){ __pid_t pid; pid = fork(); if(pid == -1){ perror("fork error"); exit(1); }else if(pid > 0){ sleep(1); printf("这里是父进程"); }else { execlp("ls", "ls", "-a", "-l", NULL); } return 0; }
|
这里解析一下execlp的使用:
第一个参数表示的是执行的是ls这个文件
第二个参数表示argv[0],第三个就是argv[1]依次类推
直到选项参数结束为NULL
假设我们要执行我们自己的程序
可以使用execl (不用指定环境变量)
除了以上的execlp和execl
还有execv和execvp, execvpe, execle
参数:
- l(list) 参数列表
- p(path) 参数的环境变量path
- v(vector) 参数的数组
- e(environment) 参数的所有的环境变量
回收子进程
孤儿进程
没爹没妈的进程
没有父进程回收,系统会将其父进程设置为init进程,可以理解为孤儿院领养了
1
| 让父进程先死,后子进程打印ID,打印的父进程ID是init进程ID
|
僵尸进程
父进程忘记给子进程结束后收尸了
子进程会残留在PCB中,成为僵尸进程,kill无法终止
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main(){ __pid_t pid; pid = fork(); if(pid < 0){ perror("fork error"); exit(1); } if(pid == 0){ printf("我是子进程,pid: %u,父进程ppid: %u\n", getpid(), getppid()); sleep(5); printf("子进程:啊,我死了\n"); }else{ while (1){ printf("我是父进程,pid: %u,我的子进程ppid: %u\n", getpid(), pid); printf("主进程发呆中呀~\n"); sleep(1); } } return 0; }
|
在终端查看进程可以看见
![](https://blog-1253996024.cos.ap-beijing.myqcloud.com/img/image-20220808141216762.png)
看见一个状态为Z+ 进程名字使用中括号括起(一般买书的时候发现作者名字被中括号扩起说明作者已经过世了)
那么该怎么将上面这两种子进程回收呢
我们需要使用以下函数
wait函数
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main(){ __pid_t pid; __pid_t wpid; int status; pid = fork(); if(pid < 0){ perror("fork error"); exit(1); } if(pid == 0){ execl("02app", "02app", NULL); printf("我是子进程,pid: %u,父进程ppid: %u\n", getpid(), getppid()); sleep(20); printf("子进程:我死了\n"); exit(121); }else{ printf("我是父进程,pid: %u,我的子进程ppid: %u\n", getpid(), pid); sleep(1); wpid = wait(&status); if(wpid < 0){ perror("wait error"); exit(1); } if(WIFEXITED(status)){ printf("正常关闭,关闭的返回值为:%d\n", WEXITSTATUS(status)); } if(WIFSIGNALED(status)){ printf("异常关闭,受到的是信号为 %d 号\n", WTERMSIG(status)); } } return 0; }
|
实际上可以这么说,几乎所有进程的异常终止,都是收到信号的影响
问题来了
一次wait函数调用能回收几个子进程?一个,想要回收多个子进程,需要多次调用
但是自动默认选择的是先结束的子进程。
显然这是有些局限的,不能指定回收哪一个子进程
问题解决:
waitpid函数
作用与wait大体相同,不同点,也是关键点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main(){ int i = 0; __pid_t pid, wpid; int count = 5; __pid_t wpid3; for ( i = 0; i < count; i++) { pid = fork(); if(pid == 0){ break; }else if(pid == -1){ perror("fork error"); exit(1); }else if(i == 2){ wpid3 = pid; } } if(i < count){ sleep(i); printf("我是第%d子进程,我的pid是: %u,我的父进程id是: %u\n", i+1, getpid(), getppid()); }else{ do{ wpid = waitpid(wpid3, NULL, WNOHANG); } while (wpid != wpid3); sleep(20); printf("第三个进程结束成功回收\n"); } printf("所有进程都回收成功"); return 0; }
|
如果要回收所有的子进程并且不挂起,将waitpid的第一个参数和跳出轮询的判断设置为 -1(-1表示没有一个进程需要回收,如果收成功,返回的是回收成功的pid)
第一个参数有四个特殊的值
- 大于0,表示回收指定的PID的进程
- 0 ,回收本组的任意一个子进程
- -1,表示回收任意一个子进程,等价于 wait(NULL);
- 小于-1的值,表示回收指定的进程组GPID的任意子进程
总结
假设需要定义一个进程任务,任务内容要求主进程有三个子进程
第一个子进程实现ps_dup2,第二个子进程实现自定义的程序,第三个出段信号引发错误的程序。
最后主进程再不结束运行的情况下对所有进程进行回收
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <fcntl.h>
void w1(){ printf("进程1"); int fd; fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0777); if(fd < 0){ perror("open"); exit(121); } dup2(fd, STDOUT_FILENO); execlp("ps", "ps", "aux", NULL); close(fd); } void w2(){ printf("进程2"); execl("w2", "w2", NULL); } void w3(){ printf("进程3"); execl("w3", "w3", NULL); }
int main(){ int i = 0; pid_t pid, wpid; int status; int count = 3; pid_t w3pid; int n=0; for (; i < count; i++) { n += 1; pid = fork(); if(pid == 0){ break; }else if(pid == -1){ perror("fork error"); exit(1); }else if(i == 2){ w3pid = pid; } } if(i == 0){ w1(); }else if(i == 1){ w2(); }else if(i == 2){ w3(); }else{ printf("轮询次数为:%d\n", n); do{ wpid = waitpid(-1, &status, WNOHANG); if(wpid > 0){ n--; if(WIFEXITED(status)){ printf("回收值:%d\n", WEXITSTATUS(status)); } if(WIFSIGNALED(status)){ printf("异常信号为:%d\n", WTERMSIG(status)); } } }while(wpid != -1 || n > 0 ); sleep(10); }
printf("主程序时间已过,再次自动回收所有的子进程\n"); printf("轮询次数为:%d\n", n); return 0; }
|