本文对 Linux 系统编程的进程相关知识进行总结,包含了进程的创建方法、IPC 实现等。
DOS
操作系统CPU
轮转CPU
内部,完成虚拟内存与物理内存的映射和设置修改内存访问级别PCB: 进程控制块,定义在 /usr/src/linux-haeders-3.16.0-30/include/linux/sched.h
中
查看资源上限的命令: ulimit -a
PCB的组成:
环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:
- 存储形式: 与命令行参数类似,
char *[]
数组,数组名environ
,内部存储字符串,NULL
作为哨兵结尾- 使用形式: 与命令行参数类似
- 加载位置: 与命令行参数类似,位于用户区,高于
stack
的起始位置- 引入环境变量表,必须声明环境变量,extern char ** environ
/bin/bash
echo $PATH
打印当前的PATH
变量
通过 man [函数名] 可以查看函数相关 API
pid_t fork(void),创建一个子进程,返回值有两个(一个进程变为两个进程,各自的 fork()
都返回):返回子进程的 PID
(非负整数)和返回 0。可以判断返回值确定子进程执行的代码或是父进程执行的代码
使用以下语句
for (i=0; i
并不是创建 N 个子进程,而是 (2^N-1)个子进程,正确的做法是在循环体中判断,如果是子进程(返回值=0),那么就 break
ps aux 显示所有进程
unistd.h 是 UNIX 系统标准库头文件
vim下使用:vs可以分屏
父子进程共享之后的异同:
相同点:
.data
.text
不同点:
注意:
1. 子进程并非将空间完全拷贝一份,而是遵循读时共享写时复制的原则
2. 父子进程共享文件描述符(所以进程通信可以通过文件共享方式实现)和 MMAP 建立的映射区
第一步,在 gcc 编译选项中增加 -g 选项;第二步,gdb 运行程序
通过 set follow-fork-mode child
跟踪子进程,通过 set follow-fork-mode parent
跟踪父进程,默认跟踪父进程
fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。
将当前进程的.text、.data 替换为所要加载的.text、.data,然后让进程从新的.text 第一条指令开始执行,但进程 ID 不变,换核不换壳。
其中有六种以 exec 开头的函数,统称 exec 函数:
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
借助环境变量表
- argv[0] 是程序名,arg[1~n-1] 是携带参数,arg[n] 是 NULL 结束符
- exec 族函数只在失败时才有返回值,成功无返回值,也不会继续再执行下面的程序
引入: 将当前的进程信息输出到文件
ps aux > out.txt
命令可以实现,但是 >
符并不属于参数,需要转义才可以DUP2
函数实现文件输出拷贝int dup2(int oldfd, int newfd);
将输出指针 oldfd 复制到 newfd,即 newfd 所指向的文件和 oldfd 所指向的文件是一样的,也就实现了 newfd 重定向到 oldfd。
需要添加头文件 fcntl.h
特别注意,僵尸进程是不能使用 kill 命令清除掉的,因为 kill 命令只是用来终止进程的,而僵尸进程已经终止。
ps aux 命令显示的进程列表中,STATE 栏表示当前状态,R 表示运行,S 表示后台运行,Z 表示僵尸进程
一个进程在终止时会关闭所有的文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号信息。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息,然后彻底清除这个进程。我们知道一个进程的退出状态可以在 Shell 中用特殊变量$?查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出状态,同时彻底清除掉这个进程。
pid_t wait(int *status);
成功返回清理掉的子进程 ID,失败返回-1(没有子进程)
父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:
当进程终止时,操作系统的隐式回收机制会完成:
可使用 wait 函数传出参数 status 来保存进程的退出状态,借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
作用同 wait,但可以指定 pid 进程清理,可以不阻塞
pid_t waitpid(pid_t pid, int *status, in)
成功返回清理掉的子进程 ID,失败返回-1(无子进程)
参数 pid:
<-1 回收指定进程组内的任意子进程
参数3:
0,阻塞回收
返回值:
一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环
Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)
在进程间完成数据传递需要借助操作系统提供特殊的方法,如文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用,现今常用的进程间通信方式有:
管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一个管道,有如下特质:
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4K)实现
管道的局限性:
常见的通信方式有,单工通信、半双工通信、半双工通信
Linux 中的7中文件类型
- 文件 d 目录 l 符号链接 s 套接字 b 块设备 p 管道
前三种才占用存储空间,后四种称之为伪文件
int pipe(int pipefd[2]);
成功: 0;失败: -1
函数调用成功会在传入参数返回 r/w 两个文件描述符,无需 open,但需 close
使用文件进行进程间通信
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
成功,返回创建的映射区首地址;失败,返回 MAP_FAILED 宏
参数:
unlink(filename) 函数,删除零食临时文件目录项,使之具备被释放条件
truncate() 和 ftruncate() 两个函数可用于改变文件长度
父子进程共享的内容有:
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));