进程和程序
- 程序:只占用磁盘空间
- 进程:运行起来的程序,占用内存、CPU等系统资源
进程控制块PCB
- 进程id:系统中每个进程有唯一的id,在C语言中用
pid_t
类型表示,其实就是一个非负整数 - 进程的状态:有就绪、运行、挂起、停止等状态
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录
- umask掩码
- 文件描述符表,包含很多指向file结构体的指针
- 和信号相关的信息
- 用户id和组id
- 会话(Session)和进程组
- 进程可以使用的资源上限
进程控制
fork函数
创建一个新的进程
1 | pid_t fork(void) |
1 |
|
getpid
获取子进程
getppid
获取父进程
1 |
|
执行代码后发现结果如下,在子进程中得到的父进程的pid为1。按理应该得到的结果是21237才对。最后查阅相关信息发现,由于父进程先退出了,造成了子进程被init(ID=1)
接管,所以得到的结果是1。这里采用的解决办法是在父进程中sleep几秒,让父进程晚于子进程结束即可。
1 | before fork-1--- |
循环创建多个子进程
1 |
|
getuid
获取用户id
getgid
获取组id
进程共享
父子进程之间在fork后,有哪些异同
刚fork之后,相同之处,全局变量、.data
、.text
、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式
不同之处,进程ID,fork返回值,各自的父进程ID,进程运行时间,闹钟(定时器),未决信号集
通过程序判断父子进程是否共享全局变量
1 |
|
结果如下
1 | I'm child pid=23313,ppid=23312 |
我们可以得到结论,父子进程间遵循读时共享写时复制的原则,这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
父子进程共享
- 文件描述符(打开文件的结构体)
- mmap建立的映射区
gdb调试父子进程
注意以下两种设置在fork函数调用之前才有效
1 | set follow-fork-mode child #命令设置gdb在fork之后跟踪子进程 |
exec函数族
该函数族可以将当前进程的.text
、.data
替换为所要加载的程序的.text
、.data
,然后让进程从新的.text
第一条指令开始执行,但进程ID不变,换核不换壳。
execlp函数
加载一个进程,借助PATH环境变量
1 | int execlp(const char *file,const char *arg,...) #成功:无返回 失败:返回-1 |
使用execlp函数执行ls
命令
1 |
|
execl函数
1 | int execl(const char *path,const char *arg,...) #成功:无返回 失败:返回-1 |
使用execl函数执行一个a.out
文件
1 |
|
组合练习
将ps
命令执行后的结果写入ps.out
文件中
1 |
|
exec函数族一般规律
exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()
和exit()
,无需if判断。
1 | l(list) 命令行参数列表 |
回收子进程
孤儿进程
父进程先于子进程结束,则子进程称为孤儿进程,子进程的父进程称为init
进程,称为init
进程领养孤儿进程
僵尸进程
进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程
wait函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但他的PCB还保留着,内核在其中保留了一些信息,如果是正常终止则保存着退出状态,如果是异常终止则保留着导致该进程终止的信号是哪个,这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息,该函数有三个功能
- 阻塞等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态
1 | pid_t wait(int *status) |
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因,宏函数可分为如下三组
1 | WIFEXITED(status) 为非0 进程正常结束 |
如下代码可以判断子进程的退出状态,并获取相应的状态信号编号
1 |
|
waitpid函数
1 | pid_t waitpid(pid_t pid,int *status,int options) |
指定某一个进程进行回收
1 |
|
指定回收所有进程
1 |
|
管道
内核借助环形队列机制,使用内核缓冲区实现
pipe函数
1 | int pipe(int fd[2]) |
pipe函数实现父进程写,子进程读的功能
1 |
|
父子进程实现ls | wc -l
1 |
|
兄弟进程实现ls | wc -l
1 |
|
管道的优劣
优点:简单,相比信号,套接字实现进程间通信,简单的多
缺点:只能单向通信,双向通信需建立两个管道,只能用于父子,兄弟等有血缘关系之间的通信
FIFO
也被称为有名管道
创建fifo
1 |
|
fifo实现非血缘关系之间的进程通信
第一个文件是写入fifo文件的代码fifo_w.c
1 |
|
第二个文件是写入fifo文件的代码fifo_r.c
1 |
|
存储映射
mmap函数
创建共享内存映射
1 | void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset) |
mmap函数建立文件映射区
1 |
|
mmap注意事项
- 创建映射区的过程中,隐含着一次对映射文件的读操作
- 当
MAP_SHARED
时,要求:映射区的权限应<=文件打开的权限(出于对映射区的保护),而MAP_PRIVATE
则无所谓,因为mmap中的权限是对内存的限制 - 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭
- 特别注意,当映射文件大小为0时,不能创建映射区,所以,用于映射的文件必须要有实际大小
- munmap传入的地址一定是mmap的返回地址,坚决杜绝指针++操作
- 文件偏移量必须为4K的整数倍
- mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功在进行后续操作
父子进程间mmap通信
代码练习,父进程创建映射区,然后fork子进程,子进程修改映射区内容,之后父进程读区映射区内容,查看是否共享
1 |
|
非血缘关系进程间通信
代码练习,两个进程,一个对结构体进行修改后写入,另一个进程对写入后的结构体进行读取
第一个文件是写入文件的代码mmap_w.c
1 |
|
第二个文件是读取文件的代码mmap_r.c
1 |
|