fork 函数
fork 函数用于创建一个进程(创建当前调用此函数的子进程)
- 父进程的fork返回 子进程ID ;而子进程的fork返回 0,表示进程创建成功
- getpid函数返回调用此函数的进程ID;getppid函数返回调用此函数的父进程ID
- getuid函数返回当前进程的实际用户ID;geteuid函数返回当前进程的有效用户ID
- 在fork之后,父子进程有相同的权利去抢占CPU,并不意味着父进程的优先级一定会比子进程的高,取决于内核所使用的调度算法
- fork函数遵循 “
读时共享,写时复制
” 的原则:如果fork后的代码只有读操作,那么共享同一块物理地址
;否则复制一份
循环创建多个进程示例:
- 用
for() { fork(); }
循环调用fork函数会创建 2n - 1 个子进程。正确的方法应该如下:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid;
int i;
for (i = 0; i < 5; ++i) {
pid = fork();
if (pid == -1) {
perror("fork error");
exit(1);
} else if (pid == 0) {
break;
}
}
if (i < 5) {
sleep(i); // 按创建子进程的顺序打印
printf("I'm %d child, pid = %u, ppid = %u\n", i+1, getpid(), getppid());
} else {
sleep(i);
printf("I'm parent, pid = %u, ppid = %u\n", getpid(), getppid()); // 这里的ppid是bash/shell
}
return 0;
}
在fork以后,父子进程有哪些相同,哪些不同呢?各自独立和共享的又有什么呢?
- 相同:代码段、堆、栈、用户ID、进程工作目录、信号处理方式等
- 不同:进程ID、父进程ID、定时器、未决信号集等
- 全局变量位于data段,父子进程是各自独立的(在做写操作的情况下,否则是共享的)
- 共享的是
文件描述符
和mmap建立的映射区
gdb调试多进程程序
gdb在调试时默认跟踪父进程,在gdb中设置fork之后的跟踪进程的方法(要在fork函数调用前设置)是
set follow-fork-mode child
set follow-fork-mode parent
子进程状态(孤儿与僵尸)
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程(1号)
僵尸进程:进程终止,父进程尚未回收,子进程0-4G进程地址空间都释放了,但唯独PCB残留在内核中,成为僵尸进程。
僵尸进程不能被kill命令清除,因为kill命令是用来终止进程的,而僵尸进程已经终止
。除非把其父进程kill掉,让它变成孤儿进程,被init进程回收- PCB存在的意义是为了父进程给子进程“收尸”,例如获取
子进程的退出状态
和导致终止的信号信息
子进程回收
对于已经终止的子进程,它的父进程可以调用 wait 或 waitpid 来获取子进程终止时在内核中残留的PCB信息,然后彻底清除掉这个进程。
1、wait 函数
- 父进程阻塞,等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态或退出原因
1)进程正常结束:
WIFEXITED(status)
为真
WEXITSTATUS(status)
,程序正常结束,用该宏函数获取进程的退出状态2)进程异常终止:
WIFSIGNALED(status)
为真
WTERMSIG(status)
,程序异常终止,用该宏函数获取导致终止的信号编号
2、waitpid 函数
-1
作为参数1可以设置waitpid函数为回收所有子进程0
做为参数1可以设置waitpid函数回收和当前进程一个组的所有子进程<-1
作为参数1可以设置waitpid函数回收指定进程组内的子进程WNOHANG
作为参数3可以设置waitpid函数为非阻塞,此时如果子进程还没结束,返回0
0
作为参数3可以设置waitpid函数为阻塞
exec 函数族
作用是在程序中运行一个程序。当一个进程调用其中一个exec函数时,该进程的用户空间代码和数据完全被新程序替换(.text,.data)
,从新程序的启动例程(.text第一条指令
)开始执行。
调用exec并不创建新进程
,所以调用exec前后该进程的ID并没有改变
示例:int execlp(const char* file, const char *arg, ...);
,命令行参数以NULL
结尾
execlp("ls", "ls", "-l", NULL);
,第二个参数是argv[0]
,第二个以后的参数是传递给可执行程序 ls;使用程序名在PATH环境变量中搜索,有环境变量参与。通常用来调用系统可执行程序。execl("/bin/ls", "ls", "-l", NULL);
,使用参数1给出的绝对路径搜索,没有环境变量参与。通常用来调用用户自定义可执行程序。
使用实例:利用exec函数,将当前系统中的进程信息打印到文件中
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main () {
int fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open ps.out error");
exit(1);
}
dup2(fd, STDOUT_FILENO); // 把参1的文件描述符复制给参2,导致参2指向的文件现在指向参1指向的文件
execlp("ps", "ps", "ax", NULL);
perror("exec error"); // exec出错才会执行该语句
exit(1);
return 0;
}
管道
管道的实质其实是一个伪文件(套接字、块设备、字符设备和管道四种文件类型都是伪文件,不占用磁盘存储空间)
- 由一个表示读端和一个表示写端的两个文件描述符所引用,数据从管道的写端流入,从读端流出
- 内核的实现方式是环形队列,数据先进先出,默认大小是4k
- 数据不能够重复读取,读走就没了。而且数据的流动具有单向性(半双工)
- 在有血缘关系的进程用管道做通信时,为了保证数据的单向流动,在定义好读写端之后需要在相应进程中关闭相应的一端
共享内存和mmap
- mmap创建映射区,返回操作这个映射区的首地址指针。munmap释放映射区
- 创建映射区(在内核)的权限要小于等于创建的文件(在磁盘)的权限,创建映射区时会对文件进行一次读操作
- 映射文件的偏移offset一定是4k的整数倍(内核中的MMU来帮助完成映射)
MAP_ANONYMOUS
这个宏实现匿名映射,就不用多余创建一个文件(然后unlink)了。常用在父子进程通信