6、C/C++——Linux进程
1、进程和线程:
1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
4. 调度和切换:线程上下文切换比进程上下文切换要快得多(不需要地址映射的切换)
线程之间共享的资源:
a、堆 由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的
b、全局变量 它是与具体某一函数无关的,所以也与特定线程无关
c、静态变量 虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的
d、文件等公用资源 这个是共享的,即线程共享打开的文件描述符
线程之间独占的资源:
a、栈 函数的局部变量与函数定义数据。
b、寄存器 其实线程里存放的是副本,包括程序计数器PC
2、并发与并行:
并发:在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但,任一个时刻点上仍只有一个进程在运行。 例如,当下,我们使用计算机时可以边听音乐边聊天边上网。 若笼统的将他们均看做 一个进程的话,为什么可以同时运行呢,因为CPU的并发机制--分时复用。
并行:得益于多核CPU,支持多个任务同时进行处理,假设并发是在一个窗口轮流办理业务,则并行则是在并发的基础上,拥有多个窗口
3、单道/多道程序设计:
单道程序设计:所有进程一个一个排队执行。若 A 阻塞,B 只能等待,即使 CPU 处于空闲状态
多道程序设计 :在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证,时钟中断(分时)即为多道程序设计模型的理论基础。
4、进程状态:
进程基本的状态有 5 种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看:
5、CPU和MMU(内存管理单元):
进程:运行起来的程序:占用CPU、内存等系统资源。
时钟中断即为多道程序设计模型的理论基础。
虚拟内存和物理内存的映射关系
MMU:内存管理单元
PCB进程控制块:
PCB进程控制块包含:
- 进程id,用pid_t类型表示
- 进程的状态,就绪、运行、挂起或者停止态
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间的信息(虚拟地址和物理内存的映射关系)
- 描述控制终端的信息
- 当前工作目录
- unmask掩码
- 文件描述符表,包含很多指向file结构体的指针
- 和信号相关的信息
- 用户id和组id
- 会话和进程组
- 进程可以使用的资源上限
进程状态:
进程基本的状态有 5 种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看
6、环境变量:
按照惯例,环境变量字符串都是 name=value 这样的形式,大多数 name 由大写字母加下划线组成,一般把 name 的部分叫做环境变量,value 的部分则是环境变量的值。环境变 量定义了进程的运行环境,一些比较重要的环境变量有
- PATH: PATH环境变量的值可以包含多个目录,用冒号隔开。
- SHELL:通常为/bin/bash
- TERM: 当前终端类型,图形界面终端下为xterm
- LANG: 语言环境
- HOME: 用户家目录
env:查看全部环境变量
char *getenv(const char *name);
获取环境变量值,成功:返回环境变量的值;失败:NULL (name 不 存在)
int setenv(const char *name, const char *value, int overwrite);
设置环境变量的值,成功:0;失败:-1
参数 :
- name::待修改的环境变量名
- value:待修改的环境变量值
- overwrite:
- 1:覆盖原环境变量
- 0:不覆盖
int unsetenv(const char *name);
删除环境变量 name 的定义,成功0,失败-1
7、进程控制:
(1)fork相关函数:
pid_t fork(void);
头文件:#include <sys types.h=""> #include </sys>
一次fork调用两次返回。
代码演示:
其中fork()两次返回并没有先后的顺序。
其中pid=0,表示的是子进程,使用getpid()、getppid()获取当前子进程的进程号和其父进程号。
其中pid>0,表示的是父进程,pid是子进程号,getpid()获取当前进程号,getppid()获取父进程的父进程。
输出结果:
getpid函数:获取当前进程id;getpid()
getppid函数:获取父进程id;getppid()
getuid函数:获取用户id
getgid函数:获取组id
(2)循环创建多个子进程fork():
#include<stdio.h> #include<string.h> #include<unistd.h> #include<pthread.h> #include<sys/types.h> int main(int argc,char *argv[]){ int i; for(i=0;i<5;i++){ if(fork()==0) break; } if(i==5){ sleep(5); printf("I'm parent!\n"); }else{ sleep(1); printf("I'm %dth child!\n",i+1); } return 0; }
(3)进程共享:
Q:父子进程之间在fork后。有哪些相同,那些相异之处呢?
A:刚 fork 之后:
- 父子相同处: 全局变量、.data段、.text段、栈、堆、环境变量、用户 ID、宿主目录、进程工作目录、信号处理方式... (0-3G)
- 父子不同处: 1.进程 ID 2.fork 返回值 3.父进程 ID 4.进程运行时间 5.闹钟(定 时器) 6.未决信号集
Q:似乎,子进程复制了父进程 0-3G 用户空间内容,以及父进程的 PCB,但 pid 不同。真的每个fork一个子进程都要将父进程的 0-3G 地址空间完全拷贝一份,然后在映射至物理内存吗?
A:当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap 建立的映射区 (进程间通信详解)
特别的,fork 之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
(4)父子进程gdb调试:
(5)exec函数族:
作用:exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
fork创建子进程后执行的是和父进程相同的程序(但是有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的和用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec函数并不创建新进程,所以调用exec前后进程的id并未改变。
当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,单进程id不变,换核不换壳。
execlp函数:
加载一个进程,借助于PATH环境变量。
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
[NULL表示变参结束]
成功:无返回
失败:返回-1
参数1:要加载的函数名。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
该函数通常用来调用系统函数:ls、data、cp、cat等命令。
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<pthread.h> int main(int argc,char *argv[]){ pid_t pid=fork(); if(pid==-1){ perror("fork errpr!\n"); exit(1); }else if(pid==0){ printf("I'm child:%d \n",getpid()); execlp("ls","ls","-l",NULL); perror("execlp errpr!\n"); exit(1); }else if(pid>0){ sleep(1); printf("I'm parent:%d \n",getpid()); } return 0; }
execl函数:
int execl(const char *path, const char *arg, .../* (char *) NULL */);
成功:无返回
失败:-1
int execl(“/bin/ls”, “ls”, “-l”, ”-F” ,NULL ); 使用参数1给出绝对路径搜索。
Exec函数的一般规律:
exec函数一旦调用成功就执行新的程序,不返回。只有失败才会返回-1.所以通常我们直接在exec函数调用后直接调用perror() exit(),无需if判断。
l(list) 命令行参数列表
p(path) 搜索file时使用PATH变量
v(vector) 使用命令行参数数组
e(environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量。
事实上,只有execve是真正的系统调用,其他五个函数最终都是调用execve函数,所以execve在man手册第二卷,其他在man手册第三卷。
8、孤儿进程:
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养了孤儿进程。
9、僵尸进程:
僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB)存放在内核里,变成了僵尸进程。
10、wait()回收子进程/waitpid():
函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
pid_t wait(int *wstatus); wstatus是一个传出参数,根据其值可以显示子进程结束状态。
- 阻塞等待子进程退出
- 回收子进程的残留资源
- 获取子进程结束状态
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/wait.h> int main(void){ pid_t pid,wpid; int status; pid=fork(); if(pid==0){ printf("---child,my id = %d, going to sleep 5s\n",getpid()); sleep(5); printf("--------------------child die----------------------\n"); return 73; }else if(pid>0){ wpid=wait(&status); if(wpid==-1){ perror("wait error\n"); exit(1); } if(WIFEXITED(status)){ printf("child exit with %d\n",WEXITSTATUS(status)); } if(WIFSTOPPED(status)){ printf("child killed with signal %d \n",WTERMSIG(status)); } printf("---------------------------parent wait success----------------------------\n"); }else{ perror("fork"); exit(1); } }
获取子进程正常终止值:
WIFEXITED(status) -->为真 --> 调用WEXITSTATUS(status) -->得到子进程退出值
WIFSIGNALED(status) -->为真 --> 调用WTERMSIG(status) -->得到导致子进程异常终止的信号编号。
11、Waitpid()函数
pid_t waitpid(pid_t pid, int *wstatus, int options); options:设置非阻塞
参数pid:
>0 回收指定的某一个id的子进程
-1回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
<-1 回收指定进程组内的任意子进程。
参数status:(传出)回收进程的状态
参数options:WNOHANG指定回收方式为,非阻塞态;0表示阻塞状态
返回值:>0 表成功回收的子进程的pid
=0 函数调用时,参数3指定了WNOHANG,并且没有进程结束。
-1 失败,设置errno
一次wait/waitpid函数调用只能回收一个子进程。
ps ajx --->查看pid ppid等
wait和waitpid都是为了回收子进程退出时,残留的资源。
总结:wait、waitpid 一次调用,只回收一个子进程
想要回收多个,就需要while循环
#include<stdio.h> #include<string.h> #include<unistd.h> #include<pthread.h> #include<sys/types.h> #include<sys/wait.h> #include<stdlib.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){ //子进程跳出执行 break; } if(i==2){ tmpid=pid; printf("--------pid = %d-------------\n",tmpid); } } if(i==5){ sleep(5); printf("I'm parent,before waitpid,pid= %d\n",tmpid); wpid=waitpid(tmpid,NULL,WNOHANG); if(wpid==-1){ perror("waitpid error"); exit(1); } printf("I'm parent,wait a child finish: %d\n",wpid); }else{ //每个子进程的具体操作在此 sleep(i); printf("I'm %dth child,pid=%d\n",i+1,getpid()); } return 0; }
#include<stdio.h> #include<string.h> #include<unistd.h> #include<pthread.h> #include<sys/types.h> #include<sys/wait.h> #include<stdlib.h> int main(int argc,char *argv[]){ int i; pid_t pid,wpid; for(i=0;i<5;i++){ pid=fork(); if(pid==0){ break; } } if(i==5){ while((wpid=waitpid(-1,NULL,WNOHANG))!=-1){ if(wpid==-1){ printf("wait child %d\n",wpid); }else if(wpid==0){ sleep(1); continue; }else{ printf("wait success\n"); } } }else{ sleep(i); printf("I'm %dth child,pid=%d\n",i+1,getpid()); } return 0; }