目录
系统调用 库函数
系统调用: 访问了内核的数据结构或者访问硬件资源
库函数:c语言标准库
查看函数具体信息:终端输入命令
系统调用: man 2 fork //fork是函数名可替换
库函数: man 3 fopen //fopen是函数名可替换
1.进程相关概念
1.1程序和进程
程序:编译好的二进制文件 例如a.out,不占用系统资源(只占用磁盘空间)
进程:活跃的程序,运行起来的程序,需要消耗系统资源(内存)
- 可以把程序看作剧本,进程看作正在演的戏,演戏需要剧本,并且一个剧本可以有多场戏同时演出,所以程序与进程之间的关系是
并发:在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但任意时刻都仍只有一个进程在进行。
并行:指在同一时刻,有多条指令在多个处理器上同时执行
1.2cpu MMU 虚拟内存
cpu(中央处理器)
MMU(虚拟内存映射管理单元)
MMU作用:
- 虚拟内存与物理内存的映射
- 设置修改内存访问级别
- 借助MMU虚拟内存映射,512M的物理内存运行程序也是4G的地址空间(虚拟地址,不真正存在)
虚拟内存作用:
- 避免用户直接访问物理内存地址,防止一些破坏性操作,保护操作系统;
- 每个进程都被分配4GB的虚拟内存,用户程序可使用比实际物理内存更大的地址空间。
- 当进程需要实际访问内存时候,会由内核的[请求分页机制]产生[缺页异常]调用物理内存页。
1.3 PCB进程控制块
本质:结构体 struct tast_struct{...} ...find usr/src/ -name sched.h
存储目录位置:
PCB进程控制块用来存放进程的相关信息内容:
- 进程id: 系统中每个进程有唯一的id,一个非负整数 ps aux 返回结果里,第二列是进程id
- 文件描述符表
- 进程状态: 初始态、就绪态、运行态、挂起态、终止态。
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间映射信息(比如说这里存着虚拟地址真正对应的物理地址)
- 描述控制终端信息(是否需要终端)
- 当前进程工作目录位置
- *umask掩码 (进程的概念)(umask默认002)
- 信号相关信息资源:未决信号集、信号屏蔽字
- 用户id和组id
- 进程可以使用的资源上限
1.4 进程状态
进程状态有五种:初始态、就绪态、运行态、挂起态、终止态
- 创建状态:当一个进程被创建时,它处于创建状态。在这个阶段,操作系统为进程分配必要的资源(将代码和数据拷贝到内存,创建PCB结构体等),并为其分配一个唯一的进程标识符(PID)。
- 就绪状态:进程就绪状态是指进程已经满足了运行的条件,进程PCB被调度到CPU运行队列中,排队等待系统分配CPU资源来执行的状态。
- 运行状态:进程PCB被调度到CPU运行队列中且已被分配CPU资源,就叫做运行态。在这个阶段,进程的指令会被执行,它可以访问CPU和其他系统资源。只有运行状态下的进程才能占用CPU资源。
- 挂起状态 :当内存不足时,如果一个进程长时间不执行或者处于低优先级状态,操作系统可能会将其代码和数据置换出内存并存储到磁盘上的swap分区中。其PCB(进程控制块)仍然存在于进程表中。这是因为挂起状态只是进程的一种状态,表示进程暂时无法执行,但仍然需要保留进程的信息以便后续恢复执行。这样可以释放内存资源,给其他优先级较高的进程提供更多的执行机会。这些被置换到磁盘中的进程的状态就叫做挂起。
- 终止状态:当进程完成其任务或被操作系统终止时,它进入终止状态。在这个阶段,进程可以释放所有已分配资源,并从系统中移除。
2.环境变量
终端使用命令可以查看系统所有的环境变量
env
终端查看某条环境变量对应的值
echo $环境变量名
或
env|grep 环境变量名
环境变量语法格式
环境变量名=值:值:值:值
常用环境变量
1)PATH
可执行程序的搜索目录,可执行程序包括Linux系统命令和用户的应用程序,PATH变量的具体用法本文后面的章节中有详细的介绍。
2)LANG
Linux系统的语言、地区、字符集,LANG变量的具体用法本文后面的章节中有详细的介绍。
3)HOSTNAME
服务器的主机名。
4)SHELL
用户当前使用的Shell解析器。
5)HISTSIZE
保存历史命令的数目。
6)USER
当前登录用户的用户名。
7)HOME
当前登录用户的主目录。
8)PWD
当前工作目录。
9)LD_LIBRARY_PATH
C/C++语言动态链接库文件搜索的目录,它不是Linux缺省的环境变量,但对C/C++程序员来说非常重要,具体用法本文后面的章节中有详细的介绍。
10)CLASSPATH
JAVA语言库文件搜索的目录,它也不是Linux缺省的环境变量,但对JAVA程序员来说非常重要,具体用法本文后面的章节中有详细的介绍。
获取环境变量的函数getenv
#include <stdio.h>
#include <stdlib.h>
int main ()
{
printf("PATH : %s\n", getenv("PATH"));
printf("HOME : %s\n", getenv("HOME"));
printf("ROOT : %s\n", getenv("ROOT"));
return(0);
}
拓展:
系统环境变量、用户环境变量 参考 https://blog.csdn.net/qq_41962968/article/details/122152904
3.进程控制
3.1 fork创建子进程
pid_t fork(void)
创建子进程。父子进程各自返回。父进程返回子进程pid。 子进程返回 0.
pid_t getpid(void);获取当前进程 ID
pid_t getppid(void);获取当前进程的父进程 ID
- 父子进程共同争夺cpu资源,执行的先后顺序随机。
- 子进程fork创建出来后会继续执行后面的代码,fork之前的代码子进程不会执行但是子进程共享这个代码段
代码实现
3.1.1 打印父子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main() {
pid_t pid;
pid = fork(); //创建子进程
if (pid < 0) { //fork返回值小于0 创建子进程失败
perror("fork error");
}
if (pid == 0){ //fork返回值等于0 表示正在运行的是子进程
printf("I am child pid = %d my father pid = %d\n",getpid(),getppid());
}
else { //fork返回值大于0 表示正在运行的是父进程 返回值是子进程的Pid
printf("I am father pid = %d my father pid = %d\n",getpid(),getppid());
sleep(5); //该程序父进程的父进程可以理解为是bash终端,子进程执行结束后会通知父进程父进程也执行完就会释放资源,如果不sleep ,bash 就有可能因为父进程的通知而释放资源导致 fork出来的子进程的打印在终端提示符后
}
return 0;
}
3.1.2 循环顺序创建多个子进程(子进程不fork)
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
int main()
{
pid_t pid;
int i;
for(i=0;i<5;i++)
{
pid =fork();
if(pid==0)
{
break; //子进程跳出不参与循环,避免子进程也fork出子进程
}
}
if(5==i)//父进程会完整执行完for循环,子进程会跳出,所以只有父进程的i值才会是5
{
sleep(5);
printf("I am father\n");
}
else//子进程 提前break跳出i不等于5就会进到else 打印
{
sleep(i);
printf("I am %d child \n",i+1);
}
return 0;
}
子进程也参与循环fork"孙进程"的情况:
3.2 父子进程的比较
比较:值是否相同,共享:是否使用同一地址空间
父子进程相同: | 刚fork后。 data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式 |
父子进程不同: | 进程id、fork的返回值、各自的父进程、进程创建时间、闹钟(定时器)、未决信号集 |
3.2.1 父子进程共享
- 读时共享、写时复制。(当是读的操作的时候全局变量共用一个地址空间,当写操作的时候会进行赋值拷贝后在写入,不使用原地址空间)
- 父子进程之间不共享全局变量(意思就是父(子)进程对全局变量做了赋值,子(父)进程的全局变量不会改变。)
- 父子共享: 1、文件描述符(打开文件的结构体) 2. mmap映射区
注:fork之后父进程先执行还是子进程先执行不确定,取决于内核所使用的调度算法
4.exec函数族
调用系统命令 如“ls”或 自定义命令
fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。
创建出的子进程与父进程有相同的 data段、text段,通过调用exec函数族可以进程 ID不变的同时替换(用户空间代码和数据完全被新程序替换).text、.data为所要加载的程序的.text、.data。
int execl(const char *path, const char *arg, ...); //path是绝对路径
int execlp(const char *file, const char *arg, ...);//默认环境变量的path路径+file参数
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[])
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
例:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
int mian(int argc,char *argv[])
{
pid_t pid = fork();
if(pid == 0)
{
//execlp("命令文件名","命令名","命令参数","命令参数",.....NULL)
//execlp函数执行成功无返回值失败返回-1
execlp("ls","ls","-l","-t","r",NULL); //NULL是哨兵表示参数结束
perror("bin/ls erroor");
exit(1);
}
else(pid > 0)
{
sleep(2);
printf(" I am parent");
}
return 0;
}
5.回收子进程
父进程有义务在子进程执行结束后回收子进程,隔辈进程无需回收。
5.1进程终止
- 会关闭所有文件描述符,
- 释放在用户空间分配的内存,
- 进程PCB残留在内核中保存了一些信息:
- 如果是正常终止则保存着退出状态值,
- 如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或者waitpid获取这些信息,然后彻底清除掉这个进程。
5.2 孤儿进程 僵尸进程(主要是理解概念)
孤儿进程 :父进程先于子进程终止,子进程就变成孤儿进程了,会被init进程领养,init进程可以称作进程孤儿院。
僵尸进程:子进程终止父进程未终止,但尚未对子进程进行回收 ,在此期间子进程被称为僵尸进程(死了没人收)
终端到的僵尸进程:
5.3 wait
wait函数: 回收子进程退出资源, 阻塞回收任意一个。
pid_t wait(int *status)
参数:(输出) 回收进程的状态。
返回值:
成功: 回收进程的pid
失败: -1, errno
wait(NULL);参数传NULL表示不要子进程的pcb 退出状态 就只完成回收的作用
wait 函数作用
- 函数作用1: 阻塞等待子进程退出 (阻塞等待是指在wait()函数后面的代码不执行等子进程回收完毕后再执行)
- 函数作用2: 回收(清理)子进程残留在内核的 pcb 资源
- 函数作用3: 通过传出参数,得到子进程退出状态(正常、异常)
wait 获取子进程退出值和异常终止信号
- 首先wpid = wait(&status); 获得退出状态和子进程pid
- 其次if(WIFEXITED(status)) 为真->>调用 WEXITSTATUS(status) 得到 子进程 退出值
- 最后if(WIFSIGNALED(status)为真->>调用 WTERMSIG(status) 得到 导致子进程异常终止的信号编号
代码如5.3.1
5.3.1 wait获取子进程退出值和异常终止信号
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid,wpid;
int status;
pid =fork();
if(pid==0)
{
printf(" i am child,my id is%d\n",getpid());
printf("child die\n");
return 73;
}
else if(pid>0)
{
//wpid=wait(NULL);不关心怎么结束的
wpid = wait(&status);//等待子进程结束
if(wpid==-1)
{
perror("wait error");
exit(1);
}
if(WIFEXITED(status))//判断 子进程正常退出判断
{
printf("child exit with%d\n",WEXITSTATUS(status));
printf("------parent finish\n");
}
if(WIFSIGNALED(status))//判断 子进程异常退出判断
{
printf("child exit with%d\n",WTERMSIG(status));
}
}
else
{
perror("fork");
return 1;
}
}
5.4 waitpid函数
指定某一个进程或任一一个进程进行回收。可以设置非阻塞。(非阻塞就是waitpid()后面的代码不用等待子进程回收完就可以执行)
waitpid函数: 指定某一个进程进行回收。可以设置非阻塞。
waitpid(-1, &status, 0) == wait(&status);就是回收任意一个子进程,并且阻塞
pid_t waitpid(pid_t pid, int *status, int options)
waitpid函数参数:
- pid:指定回收某一个子进程pid
- pid > 0 待回收的子进程pid
- pid = -1 任意子进程
- pid = 0 同组的子进程(同一个父进程创建出的子进程默认会加入到一组)。
- status:(输出) 回收进程的状态。
- options:WNOHANG 指定回收方式为,非阻塞。
返回值:
返回值> 0 : 表成功回收的指定pid的子进程
返回值 =0 : 函数调用时, 参3 指定了WNOHANG, 并且,没有子进程结束。
返回值 = -1: 失败。errno
5.4.1 waitpid回收指定子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int i;
pid_t pid, wpid, tmpid;
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) { // 循环期间, 子进程不 fork
break;
}
if (i == 2) {
tmpid = pid;
printf("--------pid = %d\n", tmpid);
}
}
if (5 == i) { // 父进程, 从 表达式 2 跳出
sleep(5);
//wait(NULL); // 一次wait/waitpid函数调用,只能回收一个子进程.
//wpid = waitpid(-1, NULL, WNOHANG); //回收任意子进程,没有结束的子进程,父进程直接返回0
//wpid = waitpid(tmpid, NULL, 0); //指定一个进程回收, 阻塞等待
printf("i am parent , before waitpid, pid = %d\n", tmpid);
wpid = waitpid(tmpid, NULL, WNOHANG); //指定一个进程回收, 不阻塞
//wpid = waitpid(tmpid, NULL, 0); //指定一个进程回收, 阻塞回收
if (wpid == -1) {
perror("waitpid error");
exit(1);
}
printf("I'm parent, wait a child finish : %d \n", wpid);
} else { // 子进程, 从 break 跳出
sleep(i);
printf("I'm %dth child, pid= %d\n", i+1, getpid());
}
return 0;
}
输出:
5.5 回收多个子进程
不管是wait还是waitpid一次都只能回收一个子进程,要想回收多个,就是在父进程中加入while循环
// 回收多个子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int i;
pid_t pid, wpid;
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) { // 循环期间, 子进程不 fork
break;
}
}
if (5 == i) { // 父进程, 从 表达式 2 跳出
/*
while ((wpid = waitpid(-1, NULL, 0))) { // 使用阻塞方式回收子进程
printf("wait child %d \n", wpid);
}
*/
while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) { //使用非阻塞方式,回收子进程.
if (wpid > 0) {
printf("wait child %d \n", wpid);
} else if (wpid == 0) { //waitpid返回值是0时表示没有子进程结束
sleep(1);
continue; //跳出本次循环,进入下一次while循环
}
}
} else { // 子进程, 从 break 跳出
sleep(i);
printf("I'm %dth child, pid= %d\n", i+1, getpid());
}
return 0;
}
参考文章: https://blog.csdn.net/zhaojiazb/article/details/129375637