进程
进程和程序
程序:死的,只占用磁盘空间
进程:运行起来的程序,占用内存、cpu等系统资源
并发
任意时刻点只有一个进程在运行,分时复用cpu
单道程序设计
所有进程一个接一个执行
多道程序设计
多道相互独立的程序,相互穿插进行
MMU
PCB进程控制块
Linux内核的进程控制块是task_struct结构体
- 进程id。系统中每个进程有唯一的id,在C语言中用pidt类型表示,其实就是一个非负整数。
- 进程的状态,有就绪、运行、挂起、停止等状态。
- 进程切换时需要保存和恢复的一些 CPU寄存器。
- 描述虚拟地址空间的信息。
- 描述控制终端的信息。
- 当前工作目录(CurrentWorking Directory)。
- umask 掩码。
- 文件描述符表,包含很多指向fie 结构体的指针。
- 和信号相关的信息。
- 用户 id 和组 id。
- 会话(Session)和进程组。
- 进程可以使用的资源上限(Resource Limit)。
进程状态
环境变量
PATH:可执行文件搜索路径
SHELL:当前shell,通常为/bin/bash
TERM:当前终端类型
LANG:语言、编码
HOME:当前用户主目录
进程控制
fork 函数
创建一个子进程
pid_t fork(void);
返回值:
成功:
父进程:子进程的id
子进程:0
失败:
-1,设置errno
pid_t getpid(void); 获得进程id
pid_t getppid(void); 获得父进程id
进程共享
刚 fork 后
父子相同处:
全局变量、.data、.text、栈、堆、环境变量、用户 ID、宿主目录、进程工作目录、信号处理方式…
父子不同处:
1.进程IDI
2.fork 返回值
3.父进程 ID
4.进程运行时间
5.闹钟(定时器)
6.未决信号集
父子共享的:
文件描述符
mmap建立的映射区(进程间通信)
注意:
父子进程遵循读时共享,写时复制的原则——全局变量
躲避父子进程共享全局变量的误区
fork之后父进程先执行还是子进程先执行不确定,取决于内核所使用的调度算法
父子进程gdb调试
使用 gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置 gdb,调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。
set follow-fork-mode child 命令设置 gdb 在 fork之后跟踪子进程。
set follow-fork-mode parent 设置跟踪父进程。
注意:一定要在fork函数调用之前设置才有效。
exec函数族
fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用 exec 前后该进程的id 并未改变。
将当前进程的.text、.data 替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
其实有六种以exec开头的函数,统称exec函数:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, …, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(fonst char *file, char *const argv[]);
int execvpe(const char *path, char *const argv[], char *const envp[]);
execlp 函数
加载一个进程,借助PATH环境变量
通常用来调用系统程序
int execlp(const char *file, const char *arg, ...);
返回值:
成功:不返回
失败:-1,设置errno
示例:
execlp("ls","ls","-l","-d","-h",NULL); NULL作为哨兵表示结束
execl 函数
加载一个进程,借助路径
通常用来调用自己写的程序
int execl(const char *path, const char *arg, ...);
返回值:
成功:不返回
失败:-1,设置errno
示例:
execl("./a.out","./a.out",参数,NULL);
execl("/bin/ls","ls","-l","-d","-h",NULL);
execvp 函数
加载一个进程,借助自定义环境变量env
int execvp(fonst char *file, char *const argv[]);
示例:
char *argv[]={"ls","-l","-d","-h",NULL};
execvp("ls",argv);
回收子进程
孤儿进程
父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
僵尸进程
进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
特别注意,僵尸进程是不能使用 kill 命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。
思考! 用什么办法可清除掉僵尸进程呢?——将父进程kill
wait 函数
父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:
- 阻塞等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)
pid_t wait(int*status);
返回值:
成功:回收子进程的ID;
失败:-1(没有子进程)
宏函数:
WIFEXITED:为真,说明子进程正常终止
WEXITSTATUS:若WIFEXITED为真,获取子进程的退出状态
WIFSIGNAL:为真,说明子进程异常终止
WTERMSIG:若WIFSIGNAL为真,获得使子进程终止的信号的编号
WIFSTOPPED:为真,说明子进程处于暂停状态
WSTOPSIG:若WIFSTOPPED为真,获得使子进程暂停的信号的编号
WIFCONTINUED:为真,说明子进程暂停后已经继续运行
示例:
if(WIFEXITED(status)){ //为真,说明子进程正常终止
cout<<"child exit with"<<WEXITSTATUS(status)<<endl;
}
if(WIFSIGNALED(status)){ //为真,说明子进程异常终止
cout<<"child kill with signal "<<WTERMSIG(status)<<endl;
}
waitpid 函数
pid_t waitpid(pid_t pid, int *status, int options);
参数:
pid:
>0 回收指定id的子进程
-1 回收任意子进程
0 回收和当前调用waitpid一个组的所有子进程
<-1 回收指定进程组内的任意子进程(进程组号取反)
status:(传出)回收子进程的状态
options:
WNOHANG 指定回收方式为非阻塞
返回值:
>0:成功回收的子进程id
0:函数调用时,参数3指定了WNOHANG,并且,没有子进程结束
-1:失败,设置errno
总结:
wait 和 waitpid 一次只能回收一个进程
想回收多个,循环回收
while((wpid=waitpid(-1,NULL,0))!=-1){
cout<<"wait child "<<wpid<<endl;
}