目录
2.6.3 两种特殊的进程 : 僵尸进程和孤儿进程 ( Z状态 )
2.9.4 wait()函数 和 waitpid()函数的总结
2.9.6 waitpid()函数: 参数int option 和 返回值
话不多说, 开饭 !
1. 前言 / 背景知识
1.1 冯诺依曼体系结构
概括来说就是, 输入设备 ------> CPU -----> 输出设备
数据在计算机的体系结构中进行流动; 在流动过程中进行数据的处理.
从一个设备到另一个设备,本质上是一种拷贝.
数据在设备间的拷贝效率,决定计算机整机的基本效率.
注意:
1. 这里的存储器指的是内存
2. 不考虑缓存,这里的CPU能且只能对内存进行读写,不能访问外设( 输入输出设备 )
3. 外设( 输入输出设备 )要输入或输出数据,也只能写入内存或从内存中读取
4. 所所有设备都只能直接和内存交互
存储金字塔: 离CPU越近, 存储效率越高, 造价越高, 容量越小
( 理论上如果电脑里全是寄存器,也OK )
冯诺依曼的最大意义是在较低价格得到一台性能不错的电脑.
eg. 你登陆QQ和朋友 聊天 / 发送文件 开始, 数据的流动过程 ?
聊天: 输入设备 -----> 内存 ( QQ在内存 ) -----> CPU ( 执行加密算法 ) -----> 输出设备 ( 和网络交互 ---> 网卡 ) -----> 网络 -----> 输入设备 ( 网卡 ) -----> 内存 ( QQ预先加载 ) -----> CPU ( 解密 ) -----> 内存 ( 消息写回 ) -----> 输出设备 ( 显示器 )
发送文件: 磁盘 ( 文件在磁盘 ) -----> 文件拖到聊天框 ( 由磁盘转到内存 ) -----> 输入设备 -----> 内存 ( QQ在内存 ) -----> CPU ( 执行加密算法 ) -----> 输出设备 ( 和网络交互 ---> 网卡 ) -----> 网络 -----> 输入设备 ( 网卡 ) -----> 内存 ( QQ预先加载 ) -----> CPU ( 解密 ) -----> 内存 ( 消息写回 ) -----> 输出设备 ( 显示器 )
最后接收文件的时候, 文件也在磁盘
冯诺依曼规定, 程序要先加载到内存再运行 ; if not , 程序在磁盘.
程序 = 代码 + 数据 ( 代码和数据都要被CPU访问 )
===>>> 再次显示: CPU不直接接触外设
1.2 操作系统
1.2.1 操作系统 广义的认识 VS 狭义的认识
广义的认识: 操作系统的内核 + 操作系统的外壳周边程序
狭义的认识: 操作系统的内核
( what is 操作系统的外壳周边程序?
-----> 给用户提供使用操作系统的方式 eg. Winndows自带的word )
1.2.2 操作系统的概念
操作系统是一款进行软硬件资源管理的软件. (eg. 识别电脑插入U盘
Why 需要操作系统 ?
对软硬件资源进行管理 [ 手段 ] , 为用户提供稳定\安全\高效的运行环境[ 目的 ]
操作系统的核心是管理
管理的本质是对数据进行管理.
任何管理都是 先描述后组织.
例: 通讯录( 本质是对人的信息做管理 )
// 描述
struct Person{
//
//
}
// 组织 对这个数组进行增删查改
struct Person contact[100];
( 歪个楼: 容器的本质是数据结构.
封装的本质是描述对象; STL : 组织方式 ; ( 容器: 组织对象 ;
编程语言最后都要进行管理对象.
1.2.3 计算机体系结构的层状划分结构
关于本图的一些注意事项:
1. 操作系统的4个最重要的功能: 进程管理 内存管理 文件系统 驱动管理 ( 这些事操作系统最主要的功能,但不是全部的功能 )
2. 驱动层( 图中所示的驱动程序 ) : 每种硬件都有自己的驱动程序, 向操作系统提供信息 / 交流接口
驱动层是软件. 驱动层的大部分组成部分由各个硬件厂商提供.
可能有多个硬件共用一个驱动程序
what is system call ?
---------- 用户要访问操作系统, 必须使用系统调用的方式, 使用操作系统
在开发角度, 操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用. 这部分由操作系统提供的接口,叫做系统调用. ( 用户要访问操作系统, 必须使用系统调用的方式 )
系统调用在使用上,功能比较基础, 对用户的要求较高.所以有心的开发者可以对部分系统调用进行适度封装, 从而形成库. 库有利于上层用户或开发者进行二次开发.
操作系统不同, 系统调用接口不同, 函数名 和 返回值不同 ---->> 跨平台性差
C / C++ 跨平台 : eg. 向上提供的都是printf , Linux / Windows 使用的是 Linux / Windows 的系统调用
计算机管理硬件:
描述: 使用 struct 结构体;
组织: 使用链表或者其他的高级数据结构
1.3 环境变量
1.3.1 环境变量的基本概念
环境变量一般是在操作系统中用来指定操作系统运行环境的一些参数
eg. 在编写C/C++程序的时候,链接时, 从来不知道我们所链接的动态静态库在哪里,但是依然可以连接成功,生成可执行程序. 这是因为有相关的环境变量帮助编译器查找.
Linux中,存在一些全局的设置,告诉命令行解释器,去哪些路径下寻找可执行程序.
系统中的很多配置, 在我们登录Linux系统的时候, 已经被加载到bash进程中 ( 内存 ) .
bash 在执行命令的时候,因为未来会加载命令,所以会先找到命令.
最开始的环境变量不在内存中, 而是在系统中对应的配置文件中.( 环境变量默认是在配置文件中 )
配置文件: vim .bash_profile
vim .bashrc
vim /etc/bashrc
1.3.2 常见的环境变量
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录( 即: 用户登录Linux系统中的时候默认的目录 )
SHELL : 当前Shell , 它的值通常是 /bin/bash
1.3.3 与环境变量相关的命令
1. echo 显示某个环境变量
2. export 设置一个新的环境变量
3. env 显示所有环境变量
4. unset 清除环境变量
5. set 显示本地定义的shell变量和环境变量
eg. echo $NAME
NAME: 环境变量名称
$NAME : 打印环境变量的内容
内建命令: bash亲自执行;
普通命令: bash创建子进程执行
( 大部分命令都是普通命令. 内建命令不是常规的命令 )
1.3.4 环境变量的组织方式
每个程序都会收到一张环境表, 环境表时一个字符指针数组, 每个指针指向一个以 '\0' 结尾的环境字符串.
1.3.5 获取环境变量
思维上, 磁盘里环境变量加载到bash进程的内存块, 我们的运行程序是bash的子进程, 都是数据共享给子进程.
获取环境变量的两种主要方式: 方式一: main的参数列表
方式二: extern声明外部指针拿到环境表
方式一: 命令行的第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++)
{
printf("%s\n" env[i]);
}
return 0;
}
bash 进程启动的时候, 默认给子进程形成两张表: argv[ ] 命令行参数表 ; env[ ] 环境变量表( 传参给环境变量 ).
bash通过各种方式交给子进程.
命令行字符串:
eg.
./myprocess | -a | -b | -c |
argv[0] | argv[1] | argv[2] | argv[3] |
程序的路径和名称 | 与该进程匹配的选项 |
命令行参数本质是交给程序不同的选项,用以提供不同的程序功能. 命令中携带很多选项.
main的参数可带可不带.
其中, int argc : 数组中的元素个数;
char* argv[ ] : 指针数组 [ 内部都是NULL ] , ( 保存字符 / 字符串地址(首元素) ) 内部都是char*, 最后以NULL结尾
方式二 : 通过第三方变量environ获取
#include <stdio.h>
int main(int argc, chara* argv[])
{
extern char **environ;
int i = 0;
for(;environ[i]; i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表. environ没有包含在任何头文件中,所以使用的时候要用extern声明.
方式三: getenv
char* getenv(const char* string); 根据环境变量名获取其内容
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n",getenv("PATH"));
return 0;
}
1.3.6 设置环境变量 : putenv();
在C语言中,`putenv` 函数用于设置指定的环境变量。该函数可用于更改已存在的环境变量的值,或者创建一个新的环境变量。`putenv` 函数的声明如下:
int putenv(char *string);
`putenv` 函数接受一个形如`"name=value"`的字符串参数,其中`name` 是要设置的环境变量的名称,`value` 是要设置的值。如果成功设置环境变量,则返回 0;如果失败,则返回非零值。
以下是一个简单的示例使用 `putenv` 函数:
#include <stdio.h>
#include <stdlib.h>
int main() {
// Set a new environment variable
char path_string[] = "MY_PATH=/my/custom/path";
if (putenv(path_string) == 0) {
printf("MY_PATH environment variable is set successfully.\n");
// Access the newly set environment variable
char* my_path = getenv("MY_PATH");
if (my_path != NULL) {
printf("The value of MY_PATH is: %s\n", my_path);
} else {
printf("MY_PATH environment variable is not available.\n");
}
} else {
printf("Failed to set MY_PATH environment variable.\n");
}
return 0;
}
在这个示例中,我们使用 `putenv` 函数将环境变量 `MY_PATH` 设置为 `/my/custom/path`,然后通过 `getenv` 函数获取并打印出新设置的环境变量值。
请注意,`putenv` 函数可以修改当前进程的环境变量,但对于子进程或父进程的环境变量不会产生影响。如果需要传递给后续启动的子进程,可以使用 `setenv` 函数。
环境变量通常具有全局属性, 可以被子进程继承下去
本地变量只在bash内部有效,无法被子进程继承下去. ( 导成环境变量,才能被获取 )
2. 进程
歪个楼: 硬盘中的程序加载到内存中,8G内存怎么运行几十G的游戏?
==>> 分批加载, 操作系统会进行置换. 对于用户具有大小的概念, 对于操作系统都是数据块. 操作系统需要什么就加载什么.
2.1 进程的基本概念
课本概念: 在计算机科学中,进程(Process)是指计算机中已运行程序的实例。每个进程都有自己独立的内存空间,包括代码、数据、堆栈等,以及一些系统资源,如打开的文件描述符、网络连接等。进程是操作系统进行任务管理和分配的基本单位,它执行计算机程序并进行数据处理。
内核观点: 进程是党当分配系统资源(CPU时间,内存)的实体;
操作系统中,进程可以同时存在很多.
操作系统要对进程进行管理.(-->> 先描述后组织 )
2.2 PCB ---- 描述进程
PCB(process control block) / 进程控制块: 存储进程信息,可以理解为进程属性的集合.
Linux操作系统下的PCB是 task_struct .
task_struct 是Linux内核的一种数据结构, 它被加载到RAM(内存)里并且包含进程的信息.
每个 进程都有一个自己的PCB.
进程 = PCB(process control block, 内核数据结构) + 代码和数据
task_struct 内容分类 :
1. 标示符: 描述进程的唯一标示符, 用来区别其他进程.
2. 状态: 进程状态, 退出代码, 退出信号等.
3. 优先级: 相对于其他进程的优先级( 执行任务的先后顺序 )
4. 程序计数器: 程序中即将被执行的下一条指令的地址.
5. 内存指针: 程序代码和进程相关数据的指针, 和其他进程共享内存快的指针.
6. 上下文数据: 进程执行时处理器的寄存器的数据.
7. I / O状态信息: 显示的I/O请求, 分配给进程的I/O设备和被进程使用的文件列表
8. 记账信息: 处理器时间总和, 使用的时钟数总和, 时间限制, 记帐号......
9. 其他信息
2.2.1 获取进程标示符
子进程 getpid 父进程 getppid
#include <stdio.h>
#include <sys/types.h>
#include <unisted.h>
int main()
{
printf("pid : %d\n",getpid()); //获取子进程的标示符
printf("ppid : %d\n",getppid()); //获取父进程的标示符
return 0;
}
getpid 和 getppid的返回值类型是pid_t, ( 实际上是unsigned int )
进程每次启动, 对应的pid都不一样( 先启动再分配 ). 父进程的ppid始终不变.
2.3 组织进程
所有运行在系统里的进程都以task_struct链表的形式存在内核中.
可以在内核源代码中找到.
2.4 查看进程
1. 进程信息可以通过/proc系列文件夹查看
eg. 获取pid为1的进程信息, 查看 /proc/1 这个文件夹
2. 通过top , ps这些指令查看
top -p 1
ps -p 1
2.5 创建进程 fork函数
进程可以创建进程.
父进程的代码和数据从磁盘加载来, 默认情况下子进程的代码和数据继承父进程的.
代码只读. 父子进程各自独立,数据要分开.
创建子进程的目的: 子进程执行与父进程不同的代码.
2.5.1 fork函数 - 简介
#include <unistd.h>
pid_t fork(void);
fork函数是操作系统提供的函数.
fork函数从已存在的进程中创建新进程.(新建立的进程是子进程, 原进程是父进程)
返回值: 子进程返回0 ; 父进程返回子进程id(方便父进程管理子进程); 出错返回-1 ( fork有两个返回值 )
fork之后通常用 if 分流
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
//fork之后父子代码共享
if(ret < 0)
{
perror("fork");
return -1;
}
// child
else if(ret == 0)
{
printf("I am child : getpid: %d , ret: %d\n",getpid(),ret);
}
//father
else{
printf("I am father : getpid: %d , ret: %d\n",getpid(),ret);
}
return 0;
}
注意: 最后的 return 0; 父子进程都会执行.
以前学习C / C++的时候, if . else if . else . 不能同时执行 --->> 因为是单进程
进程必须具有独立性,哪怕是父子进程. 这意味着一个进程出问题不会影响其他进程.
2.5.2 fork函数的底层调用
进程调用fork, 当控制转移到内核中的fork代码后, 内核的工作如下:
- 分配新的内存块和内存数据给子进程
- 将父进程的部分数据结构内容拷贝到子进程
- 添加子进程到系统进程列表中
- fork返回,开始调度器调度
fork之前父进程独立执行, fork之后父子两个执行流分别执行.
fork之后,谁先执行完全由调度器决定.
2.5.3 fork函数的常规用法
1. 父进程希望复制自己,世的父子进程同时执行不同的代码.( eg. 父进程等待客户端请求,生成子进程执行请求. )
2. 进程要执行不同于原先的程序( eg. 子进程从fork返回之后,调用exec函数 )
对于" 子进程从fork返回之后,调用exec函数 " 的理解 :
在常规的fork用法中,一个进程创建了子进程后,子进程可以调用exec函数来执行另一个程序,这样子进程就会加载并执行新的程序,取代原始的子进程程序。
例:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("Fork failed");
return 1;
} else if (pid == 0) { // 子进程
printf("Child process\n");
// 替换当前子进程的程序为ls命令(列出当前目录文件)
execl("/bin/ls", "ls", NULL);
perror("exec failed");
return 1; // 如果exec失败,子进程返回1
} else { // 父进程
wait(NULL); // 等待子进程执行完毟
printf("Parent process\n");
}
return 0;
}
原先子进程在调用`fork()`后的程序就是实际跟随着父进程继续运行的子进程程序。这个程序就是包含在 `main()` 函数中的整个代码块。当子进程调用`exec()`函数时,它会加载并执行一个全新的程序,并不再继续执行原先子进程的程序(即`main()`函数中的代码)。 ===>>> 原本, 子进程会执行main方法中的return语句,但是现在有exec函数就不会执行return了
换句话说,通过调用`exec()`函数,子进程在执行过程中会将其自己的内存空间替换为新程序的内存空间,原先子进程的程序内容随之被新程序替代,从而实现了程序的替换和重新执行。利用这种方式,就能方便地在一个进程中执行不同的程序。
2.5.4 fork函数调用失败的原因
1. 系统有太多进程( 操作系统会干掉一些进程 )
2. 实际用户的进程数超过了限制
2.6 进程状态
Linux的进程状态实际上是task_struct 中的process status属性.
2.6.1 Linux内核源代码中关于进程状态的定义
/* The task status array is a strange "bitmap" of reasons to sleep.
* Thus "running" is zero, and you can test for combinations of others with simple bit tests.
*/
static const char* const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
2.6.2 进程状态
- R (running) 运行状态 : 进程在运行中 / 进程在运行队列中
- S (sleeping) 睡眠状态 : 进程在等待事件完成( 进程在等待资源就绪 ). 此时的睡眠有时也称为 可中断睡眠( interruptible sleep ). [ 浅度睡眠 ]
- D (disk sleep) 磁盘休眠状态 : 进程通常等待I/O结束. 有时也称为 不可中断睡眠状态 ( uninterruptible sleep ) . [ 深度睡眠 ]
如果出现D状态,大部分情况是IO设备压力过大或机器快挂了.
D状态的进程不可被杀. ---->>> 等待进程自己醒来(操作完成) / 重启
- T (stopped) 停止状态 : 进程暂停,等待进一步唤醒. ( eg. 调试的时候打断点 )
可以通过发送SIGSTOP信号给进程来停止(T)进程, 这个被暂停的进程也可以通过发送SIGSTOP信号让进程继续运行.
- X (dead) 死亡状态 : 这个状态只是一个返回状态,在任务列表中看不到这个状态.
X状态的进程由操作系统进行释放.
例 : 程序执行printf语句: 以CPU的速度, CPU绝大部分时间都在外设资源加载( S状态 ) , 只有printf语句执行的时候是运行状态( R状态 ).
2.6.3 两种特殊的进程 : 僵尸进程和孤儿进程 ( Z状态 )
Z (zombie) - 僵尸进程 :
僵死状态: 子进程退出且父进程没有读取到子进程的退出信息( 退出码和退出信号 ) [ wait()系统调用 ]就会僵死.
僵死进程会以终止状态 保存在进程表,并且会一直等待父进程读取退出信息.
===>>> 进程已经运行结束( 进程中的代码和数据已经释放 ), 但是进程还需要维持自己的退出信息,进程在自己的task-struct 中记录自己的退出信息,未来让父进程进行读取.如果没有父进程读取,僵尸进程会一直存在.
kill -9 无法杀死僵尸进程.
僵尸进程的危害:
1. 进程的退出状态必须维持,如果父进程一直不读取,那么子进程一直处于Z状态
2. PCB需要一直维护退出状态 ( 维护退出状态本身需要数据维护,也属于进程基本信息,保存在PCB(task_struct)中 )
3. 父进程不回收子进程会造内存资源的浪费(数据结构对象本身就要占用内存.)
4. 内存泄漏
孤儿进程: 父进程先退出,子进程后退出,这个孤儿进程会被1号init进程(理解为操作系统)回收,处理该进程的退出信息
2.7 进程优先级
2.7.1 进程优先级的基本概念
进程优先级: 指定进程获取某种资源的先后顺序.( Linux优先级数字越小,优先级越高. )
可以把进程运行到指定的CPU上.(不重要的进程安排到某个CPU), 可以大大改善系统性能.
饥饿问题: 进程长时间得不到对应的资源(不被调度)
操作系统关于调度和优先级的原则: 分时操作系统
what is 分时操作系统 ?
分时操作系统(Time-sharing Operating System)是一种可以使多个用户共享计算机资源的操作系统。在分时操作系统下,操作系统会给每个用户分配一定的时间片(Time Slice),这样每个用户可以轮流使用计算机,实现看似同时运行的效果(从而实现多任务并发执行)。
分时操作系统通常具有以下特点:
1. **时间片轮转**:每个用户或进程被分配一个时间片,在时间片结束之前,系统会自动切换到下一个用户或进程,从而实现多任务并发执行。
2. **多道程序设计**:分时操作系统支持多个程序同时在内存中运行,因此能够提高系统的利用率。
3. **资源共享**:多个用户可以同时访问计算机的资源,如CPU、内存、硬盘等,从而实现资源共享,提高计算机的效率和利用率。
4. **交互性强**:分时操作系统通常支持人机交互,用户可以通过终端与计算机系统进行交互操作。
5. **公平性**:分时操作系统通常会采取公平调度策略,确保不同用户或进程都能及时得到资源,并且避免某个用户或进程长时间占用资源导致其他用户无法使用的情况发生。
2.7.2 查看进程
ps -l 命令 : 查看进程
图中的几个重要信息:
- UID : 执行者的身份
- PID : 进程的代号
- PPID : 该进程的父进程的代号
- PRI : 该进程的优先级
- NI : 进程的nice值(优先级的修正数据) PRI(new) = PRI(old) + nice nice的取值范围: [-20, 19]
2.7.3 查看进程优先级的命令
用top命令更改已存在进程的nice
输入 top -->> 进入top后按"r" -->> 输入进程PID -->> 输入nice值
每次调整优先级都是从80(进程优先级的初始值)开始
2.8 Linux2.6内核进程调度队列
Linux2.6内核中进程队列的数据结构( runqueue ):
一个CPU拥有一个runqueue.
2.8.1 优先级:
- 普通优先级: [100 , 139]
- 实时优先级: [0, 99] (不关注)
2.8.2 active指针 和 expired指针:
active指针永远指向活动队列, expired指针永远指向过期队列.
活动队列的进程越来越少("只出不进"), 过期队列的进程越来越多("只进不出") --->>> 在合适的时候,交换active指针和expired指针的内容 ( Swap(active,expired); )
2.8.3 活动队列:
时间片还没有结束的所有进程都按照优先级放在活动队列.
- nr_active : 总共有多少个运行状态的进程
- queue[140] : 一个数组元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度.数组下标表示优先级.
====>>>> 在Linux 2.6内核的进程调度队列中,queue[140]
是指第 140 个优先级(priority)的就绪队列。Linux内核利用多级反馈队列调度算法来管理进程的调度。在这种算法中,进程根据其优先级被分配到不同级别的队列中,每个队列都有一个数值来表示其优先级。较低数值的队列具有较高的优先级。
what is FIFO规则 ?
FIFO规则是指先进先出(First-In-First-Out)的调度规则。在进程调度中,如果多个进程具有相同的优先级,按照FIFO规则,系统会选择最先到达就绪状态的进程进行调度和执行。也就是说,最早进入就绪队列的进程将会被首先执行,直到该进程完成或者发生阻塞。
- bitmap[5] : 位图( 位图是一种数据结构,它使用二进制位来表示某种特定属性或状态。每个位都可以表示某个进程或资源的状态,比如是否可用、是否被占用等。在一个位图中,每个位通常被分配给相应的进程或资源,以标识其特定的状态 )使用32 * 5个比特位表示队列是否为空, 0的位置表示无,1的位置表示有
2.8.4 过期队列:
过期队列和活动队列的结构完全一样.
过期队列放置的都是时间片耗尽的进程.
当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算.
2.8.5 大O(1)调度算法 :
http://t.csdnimg.cn/9JYu7 ( 参考! 指路~ )
2.9 进程等待
任何子进程在退出时, 一般都必须被父进程进行等待. (子进程本身是软件,父进程本质是等待某种软件条件就绪)
WHY ? > [ must ] 父进程通过等待,解决子进程退出的僵尸状态, 回收系统资源
[ 可选 ] 获得子进程的退出信息(了解子进程的退出原因)
HOW ? >
2.9.1 wait方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
等待父进程的任意一个子进程退出
返回值 : 成功返回被等待进程的pid, 失败返回-1
参数: 输出型参数,获取子进程的退出状态,不关心则可以设置为NULL.
2.9.2 waitpid方法
pid_t waitpid(pid_t pid,int* status,int options);
返回值: 正常返回时,返回收集的子进程的进程id;
如果设置了WNOHANG选项, 并且调用waitpid发现没有已退出的子进程可以收集,则返回0;
如果调用时出错,则返回-1; 此时errno会被设置成相应的值以指示错误原因.
参数:
pid:
pid = -1, 等待任意一个子进程,与wait等效
pid > 0, 等待其进程id与pid等效的子进程.
status:
WIFEXITED(status) : 检查进程是否正常退出. 如果子进程正常返回,则为真.
WEXITSTATUS(status) :如果WIFEXITED非0, 查看子进程的退出码.
options:
WNOHANG : 如果pid指定的子进程没有结束,则waitpid()函数返回0,不再等待.如果正常结束,则返回该子进程的pid.
2.9.3 获取子进程的status
wait 和 waitpid , 都有一个status参数. status参数是一个输出型参数,由操作系统填充.
如果传输的是NULL, 表示不关心子进程的退出状态信息 ; 否则, 操作系统会根据该参数,将子进程的退出信息反馈给父进程.
status可以当成位图看待. (只研究status低16比特位)
正常终止 : 0~7 位是0; 8~15位表示退出状态; [ 从右往左 0, 1, ......]
被型号所杀: 0 ~ 6位表示终止信号, 7~15位 未用 .
2.9.4 wait()函数 和 waitpid()函数的总结
- 如果子进程已经退出,调用wait() / waitpid()时,wait / waitpid会立即返回并释放资源, 获得子进程的退出信息.
- 如果在任意时刻调用wait / waitpid,子进程存在且正常运行, 则进程可能阻塞.
- 如果不存在该子进程,则立刻出错返回.
2.9.5 进程的阻塞等待和非阻塞等待
{ 小剧场 }
某日, 张三给李四打电话:
张三: 李哥李哥, 一块出去看电影呗~
李四: 呀, 我收拾收拾东西, 你等我一会,半个小时之后我和你碰头
张三 : 好的, 我在你宿舍楼下等你.
PART 1
张三挂断电话.
五分钟后 ------
张三 (打电话给李四) : 李哥李哥, 你好了没有?
李四 (收拾东西) : 还没呢,还在收拾东西.
张三 : 好的.
张三挂断电话.
张三掏出手机,一边等待李四, 一边玩了一把游戏.
又过了一段时间 ------
张三 (打电话给李四) : 李哥李哥, 你好了没有?
李四 (收拾东西) : 还没呢,还在收拾东西.
张三 : 好的.
张三挂断电话.
张三一边等李四,一边玩手机 ------>>>>> 非阻塞等待
PART 2
张三没有挂断电话,始终和李四保持通信状态,时刻等待李四收拾好东西.
张三一直在李四的宿舍楼下,什么事情也不做,只是专心等待李四.
王五看到了张三, 过来和张三打招呼.
张三的心里只有一件事 ----- 那就是等待李四 !
张三电话一直不挂, 只做等待李四这一件事 ----->>>>> 阻塞等待
2.9.6 waitpid()函数: 参数int option 和 返回值
默认情况下, int option = 0; (阻塞状态)
阻塞等待: 返回值 > 0 : 等待成功,子进程退出且父进程回收成功;
返回值 < 0 : 等待失败, 不再等待.
非阻塞等待 : 返回值 = 0 : 检测成功, 但是子进程未退出, 需要下一次重复等待.
tips: 非阻塞轮询
非阻塞轮询 = 非阻塞等待的时候 + 循环
非阻塞轮询(non-blocking polling)是一种用于处理I/O操作的编程模式。在传统的阻塞I/O模式中,当程序调用一个I/O操作时,程序会被阻塞,直到操作完成。而非阻塞轮询模式中,程序发出一个I/O操作后,会立即返回并继续执行其他任务,然后通过轮询的方式去查询操作是否已经完成。
在非阻塞轮询模式中,程序会周期性地查询(轮询)某个I/O操作的状态。如果操作已经完成,程序将得到相应的结果;如果操作还未完成,则程序可以继续执行其他任务。这样可以实现同时处理多个I/O操作,提高系统的并发性能。
非阻塞轮询模式通常用于事件驱动的编程中,比如网络编程中常用的轮询异步I/O模型(如epoll、kqueue等)。它可以帮助程序在等待I/O操作完成的过程中不被阻塞,从而充分利用CPU资源,提高系统的吞吐量。
需要注意的是,非阻塞轮询模式虽然可以提高系统的并发性能,但由于需要频繁地查询操作的状态,可能会导致CPU资源的浪费。因此,在使用非阻塞轮询模式时,需要权衡系统的负载和性能,并适当选择合适的策略。
2.10 进程的程序替换
2.10.1 替换原理
用fork创建子进程后执行的时和父进程相同的程序 (但有可能是不同的代码分支). 子进程往往要调用一种exec函数来执行另一个程序.当进程调用exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行. 调用exec并不创建新进程, 所以调用exec前后该进程的id没有改变.
创建子进程,让 子进程完成任务 : 1. 子进程执行父进程的一部分;
2. 子进程执行一个全新的程序;
2.10.2 替换函数
六种exec开头的函数,统称exec函数:
头文件 : #include <unistd.h>
函数名 | 函数原型 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
execl | int execl(const char* path, const char* arg, ...); | 列表 | 否 | 是 |
execlp | int execlp(const char* file, const char* arg, ...); | 列表 | 是 | 是 |
execle | int execle(const char* path, const car* arg, ... , char *const envp[ ]); | 列表 | 否 | 否, 必须自己组装环境变量 |
execv | int execv(const char* path, char *const argv[ ]); | 数组 | 否 | 是 |
execvp | int execvp(const char* file, char *const argv[ ]); | 数组 | 是 | 是 |
execve | int execve(const char* path, char *const argv[ ], char *const envp[ ]); | 数组 | 否 | 否, 必须自己组装环境变量 |
命名理解:
-
l ( list ) : 参数采用列表
-
v ( vector ) : 参数采用数组. 例 :
char *const argv[ ] =
{
(char*)"ls",
(char*)"-l",
(char*)"-a",
(char*)"--color",
NULL
};
-
p ( path ) : 自动搜索环境变量PATH
( 要执行的程序需要文件路径 ). 这意味着用户可以不传文件路径,只传要执行的文件名; 查找这个程序,系统会自动在环境变量path中 进行查找.
-
e ( env ) : 自己维护环境变量
只有execve是系统调用,其余函数最红都是调用execve. ( execve在man手册第2章,其余函数在man手册第三章 )
exec系列函数举例:
#include <unistd.h>
int main(){
char *const argv[] = {"ps","-ef",NULL};
char *const envp[] = {"PATH = /bin:/usr/bin","TERM=console",NULL};
execl("/bin/ps","ps","-ef",NULL);
// 含p, 可以使用环境变量PATH, 不用写全路径
execlp("ps","ps","-ef",NULL);
// 含e, 需要自己组装环境变量
execle("ps","ps","-ef",NULL,envp);
execv("/bin/ps",argv);
execvp("ps",argv);
execve("/bin/ps",argv,envp);
exit(0);
}
2.11 进程退出
进程终止的本质: 释放曾经的代码数据所占据的空间,释放内核数据结构
2.11.1 进程退出场景
(通过进程的退出码决定):
- 1. 代码运行结束,结果正确;
- 2. 代码运行结束,结果错误
3. 代码异常终止(未完成运行)
衡量一个进程退出的两个信号: 退出码 , 退出信号
代码运行结束的情况举例:
#include <unistd.h>
#include <stdio.h>
#include <string.h>
enum{
Success = 0,
Div_Zero,
Mod_Zero,
};
int exit_code = Success;
const char* CodeToErrString(int code){
switch(code){
case Success:
return "Success";
case Div_Zero:
return "div zero";
case Mod_Zero:
return "mid zero";
default:
return "unknown error";
}
}
int Div(int x,int y){
if(0 == y){
exit_code = Div_Zero;
return -1;
}
else
return x / y;
}
int main(){
int result = Div(10,100);
printf("result: %d[%s]\n",result,CodeToErrString(exit_code));
result = Div(10,0);
printf("result: %d[%s]\n",result,CodeToErrString(exit_code));
return exit_code;
}
2.11.2 进程的退出方法
异常退出: Ctrl + C 信号终止.
异常: 程序运行时崩溃 ----- 操作系统发现进程做了不该做的事,操作系统杀了进程.
一旦出现异常,退出码没有意义.
WHY 异常 ? : 进程收到操作系统发出的信号 (---->> 根据退出信号找到异常原因.)
segmentation fault : 段错误.(eg. 野指针) 操作系统提前终止进程
正常退出: echo $? 查看进程退出码
父进程bash获得最近的一个子进程的退出码(告诉父进程任务完成的如何)
返回值 : 0 : 成功
非0 : 标识失败
不同的非0值代表不同的错误. 在 系统层面,错误码会转化成对应的错误码描述.
2.11.3 exit函数 与 _exit函数
_exit函数
#include <unistd.h>
void _exit(int status);
参数: status 定义了进程的终止状态, 父进程通过wait来获得该值;
虽然 status 是 int , 但是仅有低八位可以被父进程所用,所以当_exit(-1)时,在终端执行 $? , 返回值是255
exit函数
#include <unistd.h>
void exit(int status);
return是一种更常见的退出进程的方法. 执行return n等同于执行 exit(n) .
因为调用int main,函数会将main的返回值作为exit的参数.
3. 进程的 阻塞 , 挂起 和 运行
( 实在是写不动了 )
( 目前还没想到哪里需要补充,等想到了再说吧...... )