初探 Linux 系统编程之进程
Posted 吴豪杰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初探 Linux 系统编程之进程相关的知识,希望对你有一定的参考价值。
0 前言
本文对 Linux 系统编程的进程相关知识进行总结,包含了进程的创建方法、IPC 实现等。
1 进程相关概念
- 单道程序设计模式:
DOS
操作系统 - 多道程序设计模式: 通过时钟中断在硬件级别控制
CPU
轮转 - MMU: 内存管理单元,位于
CPU
内部,完成虚拟内存与物理内存的映射和设置修改内存访问级别 PCB: 进程控制块,定义在
/usr/src/linux-haeders-3.16.0-30/include/linux/sched.h
中查看资源上限的命令: ulimit -a
PCB的组成:
- 进程号
- 进程的状态,有就绪、运行、挂起、停止等状态
- 进程切换时需要保存和回复的一些 CPU 寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录
- UMASK 掩码
- 文件描述表,包含很多指向 File 结构体的指针
- 和信号相关的信息
- 用户 ID 和组 ID
- 会话和进程组
- 进程可以使用的资源上限
2 环境变量
环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:
- 字符串
- 有统一的格式: 名称=值,多个值用冒号分隔
- 值用来描述进程环境信息
- 存储形式: 与命令行参数类似,
char *[]
数组,数组名environ
,内部存储字符串,NULL
作为哨兵结尾- 使用形式: 与命令行参数类似
- 加载位置: 与命令行参数类似,位于用户区,高于
stack
的起始位置- 引入环境变量表,必须声明环境变量,extern char ** environ
2.1 常见的环境变量
- PATH: 可执行文件的搜索路径,从前往后搜索,所以新版本的环境变量因放置在前面
- SHELL: 记录当前使用的命令解释器,如
/bin/bash
- HOME: 当前主目录
- LANG: 当前语言
- TERM: 当前的终端信息
echo $PATH
打印当前的PATH
变量
2.2 相关函数
- char* getenv(const char *name); 成功:返回环境变量到值,失败:NULL
- int setenv(const char *name, const char *value, int overwrite); 成功:0,失败:-1,override 取1表示覆盖原环境变量值
- int unsetenv(const char *name); 成功:0,失败:-1,当name不存在是仍然返回0,当name命名为”ABC=”时则会出错
通过 man [函数名] 可以查看函数相关 API
3 进程控制
3.1 fork 函数
pid_t fork(void),创建一个子进程,返回值有两个(一个进程变为两个进程,各自的 fork()
都返回):返回子进程的 PID
(非负整数)和返回 0。可以判断返回值确定子进程执行的代码或是父进程执行的代码
3.2 创建多个子进程
使用以下语句
for (i=0; i<n; i++)
fork();
并不是创建 N 个子进程,而是 (2^N-1)个子进程,正确的做法是在循环体中判断,如果是子进程(返回值=0),那么就 break
ps aux 显示所有进程
unistd.h 是 UNIX 系统标准库头文件
vim下使用:vs可以分屏
3.3 补充函数
- uid_t getuid(void),获取当前进程的实际用户 ID
- uid_t geteuid(void),获取当前进程的有效用户 ID
- gid_t getgid(void),获取当前进程使用组 ID
- gid_t getegid(void),获取当前进程有效用户组 ID
3.4 进程共享
父子进程共享之后的异同:
相同点:
- 全局变量
.data
.text
- 栈、堆
- 环境变量、信号处理方式
- 用户ID、宿主目录、进程工作目录
不同点:
- 进程 ID、父进程 ID
- fork 返回值
- 进程运行时间
- 定时器
- 未决信号集
注意:
1. 子进程并非将空间完全拷贝一份,而是遵循读时共享写时复制的原则
2. 父子进程共享文件描述符(所以进程通信可以通过文件共享方式实现)和 MMAP 建立的映射区
3.5 GDB 调试
第一步,在 gcc 编译选项中增加 -g 选项;第二步,gdb 运行程序
通过 set follow-fork-mode child
跟踪子进程,通过 set follow-fork-mode parent
跟踪父进程,默认跟踪父进程
4 Exec 函数族
fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。
将当前进程的.text、.data 替换为所要加载的.text、.data,然后让进程从新的.text 第一条指令开始执行,但进程 ID 不变,换核不换壳。
其中有六种以 exec 开头的函数,统称 exec 函数:
- int execl(const char *path, const char *arg, …); // list
加载一个进程,通过路径+程序名来加载,成功无返回,失败返回-1,;对比execlp,如加载”ls”命令带有-l,-F参数
execlp("ls", "ls", "-l", "-F", NULL); // 使用程序名在PATH中搜索
execl("/bin/ls", "ls", "-l", "-F", NULL); //使用参数1给出的绝对路径搜索
int execlp(const char *file, const char *arg, …); // list path
加载一个进程,借助 PATH 环境变量,成功无返回,失败返回-1;参数1:要加载到程序的名字,该函数通常用来调用系统程序,如:ls、date、cp、cat 等命令int execle(const char *path, const char *arg, …, char *const envp[]); // list environment
借助环境变量表- int execv(const char *path, char *const argv[]);
携带参数表 - int execvp(const char *file, char *const argv[], char *const envp[]);
携带环境变量表和参数表
- argv[0] 是程序名,arg[1~n-1] 是携带参数,arg[n] 是 NULL 结束符
- exec 族函数只在失败时才有返回值,成功无返回值,也不会继续再执行下面的程序
DUP2
引入: 将当前的进程信息输出到文件
- 方法一: 通过
ps aux > out.txt
命令可以实现,但是>
符并不属于参数,需要转义才可以 - 方法二: 使用
DUP2
函数实现文件输出拷贝
int dup2(int oldfd, int newfd);
将输出指针 oldfd 复制到 newfd,即 newfd 所指向的文件和 oldfd 所指向的文件是一样的,也就实现了 newfd 重定向到 oldfd。
需要添加头文件 fcntl.h
5 回收子进程
- 孤儿进程
父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为 init 进程,称为 init 进程领养孤儿进程 - 僵尸进程
进程终止,父进程尚未回收,子进程残余资源(PCB)存放于内核中,变成僵尸(Zombie)进程
特别注意,僵尸进程是不能使用 kill 命令清除掉的,因为 kill 命令只是用来终止进程的,而僵尸进程已经终止。
ps aux 命令显示的进程列表中,STATE 栏表示当前状态,R 表示运行,S 表示后台运行,Z 表示僵尸进程
5.1 Wait 函数
一个进程在终止时会关闭所有的文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号信息。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息,然后彻底清除这个进程。我们知道一个进程的退出状态可以在 Shell 中用特殊变量$?查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出状态,同时彻底清除掉这个进程。
pid_t wait(int *status);
成功返回清理掉的子进程 ID,失败返回-1(没有子进程)
父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:
- 阻塞等待子进程退出
- 回收子进程残余资源
- 获取子进程结束状态(退出原因)
当进程终止时,操作系统的隐式回收机制会完成:
- 关闭所有文件描述符
- 释放用户空间分配的内存
内核的 PCB 仍存在,其中保存该进程的退出状态(正常终止->退出值,异常终止->终止信号)。
可使用 wait 函数传出参数 status 来保存进程的退出状态,借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
- WIFEXITED(status) [wait if exited] 非零 进程正常退出,使用 WEXITSTATUS(status) 可获取进程退出状态(exit的参数)
- WIFSIGNALED(status) 非零 进程异常终止,使用 WTERMSIG(status) 取得使进程终止的那个信号的编号
- WIFSTOPPED(status) 非零 进程处于暂停状态,使用 WSTOPSIG(status) 取得使进程暂停的那个信号的编号,WIFCONTINUED(status) 如果为真说明进程暂停后已经继续运行
5.2 waitpid 函数
作用同 wait,但可以指定 pid 进程清理,可以不阻塞
pid_t waitpid(pid_t pid, int *status, in)
成功返回清理掉的子进程 ID,失败返回-1(无子进程)
参数 pid:
- >0 回收指定 ID 的子进程
- -1 回收任意进程(相当于 wait)
- 0 回收和当前调用 waitpid 一个组的所有子进程
<-1 回收指定进程组内的任意子进程
参数3:0,阻塞回收
- WNOHANG,非阻塞回收(一般使用轮询)
返回值:
- pid 成功
- -1 失败
- 0 参数3为WNOHANG 并且子进程尚未结束
一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环
6 进程间通信
Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)
在进程间完成数据传递需要借助操作系统提供特殊的方法,如文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用,现今常用的进程间通信方式有:
- 管道(使用最简单)
- 信号(开销最小)
- 共享映射区(无血缘关系)
- 本地套接字(最稳定)
6.1 管道 PIPE
6.1.1 概念
管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一个管道,有如下特质:
- 其实质是一个伪文件(实为内核缓冲区)
- 由两个文件描述符引用,一个表示读端,一个表示写端
- 规定数据从管道的写端流入管道,从读端流出
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4K)实现
管道的局限性:
- 数据自己读不能自己写
- 数据一旦被读走,便不在管道中存在,因此,数据只能在一个方向上流动
- 只能在有公共祖先的进程间使用管道
常见的通信方式有,单工通信、半双工通信、半双工通信
Linux 中的7中文件类型
- 文件 d 目录 l 符号链接 s 套接字 b 块设备 p 管道
前三种才占用存储空间,后四种称之为伪文件
6.1.2 pipe 函数
int pipe(int pipefd[2]);
成功: 0;失败: -1
函数调用成功会在传入参数返回 r/w 两个文件描述符,无需 open,但需 close
6.2 有名管道 FIFO
6.3 共享内存 MMAP
使用文件进行进程间通信
6.3.1 mmap 函数
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
成功,返回创建的映射区首地址;失败,返回 MAP_FAILED 宏
参数:
- addr 建立映射区的首地址,由 Linux 内核指定,使用时,直接传递 NULL
- length 域创建映射区的大小
- prot 映射区权限 PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
- flags 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_SHARED 会将映射区所做的操作反映到物理设备(磁盘)上
MAP_PRIVATE 映射区所做的修改不会反映到物理设备 - fd 用来建立映射区的文件描述符
- offset 映射文件的偏移(4K的整数倍)
6.3.2 注意事项
- malloc 分配内存可以分配 0 字节,也可以将其释放,但映射区并不可以分配 0 字节
- 不能对 mmap 的返回值进行修改,否则 munmap 无法成功
- 打开文件只读,则不论是否开启映射物理设备,均不能在映射区进行写操作
- 创建映射区的权限要小于等于打开映射区文件的权限,映射区的创建中隐含着一次对映射区文件的读操作
- 最后一个参数的 offset 必须要是 4k 的整数倍,即 4096 的整数倍,页大小
- 映射区大小不能大于文件大小
- 文件描述符先关闭对读写映射区操作无影响,因为现在是通过映射区读写,不再使用文件句柄
unlink(filename) 函数,删除零食临时文件目录项,使之具备被释放条件
truncate() 和 ftruncate() 两个函数可用于改变文件长度
6.3.3 父子进程共享
父子进程共享的内容有:
- 共享打开的文件
- mmap 建立的映射区(但必须要使用 MAP_SHARED)
6.3.3 匿名映射区
mmap 足够方便,但问题在于每次建立映射一定要依赖一个文件才能实现,通常为了建立映射区要 open 一个 temp 文件,从创建好了再 unlink、close,比较麻烦。于是可以直接使用匿名映射来代替,借助标志位 MAP_ANONYMOUS
或 MAP_ANON
,注意该宏仅在 Linux 操作系统中可用
MAP_ANON
宏仅在 Linux 操作系统中可用,在类 Unix 系统中如果没有该宏,可以使用 fd = open(“/dev/zero”, O_RDWR) 代替
用法
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
- 注意的是 fd 需要配置为-1
- mmap 可用于非血缘关系进程通信
- 使用 memcpy() 函数可以拷贝结构体
memcp(map, $student, sizeof(student));
以上是关于初探 Linux 系统编程之进程的主要内容,如果未能解决你的问题,请参考以下文章