Linux系统中进程可以创建子进程。
1. fork函数:创建新进程
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
/*
功能:
一个进程创建新进程。原进程为父进程,新进程为子进程。
返回值:
成功:
子进程中返回0,父进程中返回子进程的pid。
失败:-1
失败原因:
a) 进程总数达到系统上限,此时errno被设置为EAGAIN
b) 系统内存不足,此时errno被设置为ENOMEM
*/
fork示例:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int test01() {
fork(); // fork成功的话,给子进程返回0,给父进程返回子进程的pid
printf("Hello world\n");
}
运行结果:
pc指针:指向当前运行指令的下一条指令。
fork时,父进程的pc指针也会复制,因此子进程会从fork后的指令开始执行。
2. exit和_exit函数:结束进程
#include<stdlib.h>
void exit(int status); // 标准库函数
/*******************************************/
#include<unistd.h>
void _exit(int status); // 系统调用
/*
功能:
调用结束次函数的进程。
参数:
status:返回给父进程的参数(低8位有效),
此参数根据需求填写;
例如,写123,则正常退出时会传递状态码123;
若被信号终止,则传递的退出码就是信号的编号,而不再是123.
*/
exit和_exit的区别:
exit会刷新缓冲区、关闭文件描述符:
exit和_exit区别示例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc, const char* argv[]) {
printf("Hello.");
_exit(0); // 什么也不输出。
//exit(0); // 输出Hello. exit会刷新缓冲区
//return 0; // 输出Hello. return 0也会刷新缓冲区
}
/*
注:若printf中有\n,则_exit(0)也会输出Hello.
因为\n在标准输出中具有刷新缓冲区的作用.
*/
return也可结束进程,return和exit的区别是:
若一个进程先后调用funcA,funcB。
若funcA中使用exit结束,则该进程立即结束,不会再调用funcB;
若funcA中使用return结束,则表示该funcA函数结束,仍会继续调用funcB.
3. wait和waitpid
进程退出时,内核释放该进程大部分资源,包括打开的文件、占用的内存等。但仍保留了该进程的PCB信息,因此需要父进程通过wait和waitpid函数来进一步回收,否则这些进程会变成僵尸进程,消耗系统资源。
(1)wait函数:
阻塞等待子进程退出,回收子进程资源。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
/*
功能:
等待任意一个子进程结束,回收该子进程资源,并传出子进程退出的状态到status。
参数:
status:存储进程退出时的状态信息。
返回值:
成功:被回收的子进程号
失败:-1
*/
设置退出时的状态信息status使用方式:
(1)若WIFEXITED(status)非0:
表示进程正常退出,可使用WEXITSTATUS(status)获取进程退出状态码(即exit的参数)。
(2)若WIFSIGNALED(status)非0:
表示进程异常终止(被信号杀死),可使用WTERMSIG(status)获取终止信号的编号。
(3)若WIFSTOPPED(status)非0:
表示进程被暂停,可使用WSTOPSIG(status)获取暂停信号的编号。
(4)若WIFCONTINUED(status)非0:
表示进程暂停后被继续运行。
调用wait函数的进程会被阻塞,直到有一个子进程退出或收到一个不能被忽视的信号。
若调用wait的进程无子进程,wait函数会立即返回;若子进程早就结束,则wait函数也会立即返回,并且回收该早就结束的子进程。
若参数status的值不为NULL,wai函数会把子进程退出时的状态(int型数值)存入status,指出子进程是否为正常结束。该退出的状态信息。
wait和获取子进程退出状态信息示例:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main(int argc, const char* argv[]) {
pid_t pid = -1;
int ret = -1;
int status = 0;
pid = fork();
if (-1 == pid) {
perror("fork");
return 1;
}
if (0 == pid) { // 子进程
printf("子进程%d运行...\n", getpid());
sleep(10);
exit(10); // 子进程终止。指定状态码为10.
}
// 父进程
printf("父进程执行。等待子进程退出,回收其资源...\n");
// 父进程阻塞,等待子进程退出
ret = wait(&status);
if (-1 == ret) {
perror("wait");
return 1;
}
printf("父进程回收子进程%d的资源...\n", ret);
// 获取子进程退出的状态
if (WIFEXITED(status)) { // 子进程正常退出
printf("子进程正常退出,状态码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程被信号%d杀死了...\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf("子进程被信号%d暂停...\n", WSTOPSIG(status));
}
return 0;
}
正常退出,显示指定的状态码10,运行结果:
在另一个终端使用kill -9杀死该进程,运行结果:
在另一个终端直接使用kill杀死该进程,运行结果:
在另一个终端直接使用kill -19暂停该进程,运行结果:
(2)waitpid函数:
阻塞等待子进程退出,回收子进程资源;也可设置非阻塞,无子进程退出则立即返回。
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
/*
功能:
等待子进程结束,回收该子进程资源,并传出子进程退出的状态到status。
参数:
pid:
> 0:等待进程号为pid的子进程退出;
= 0:等待同一个进程组中的任何子进程退出;若子进程已加入其他进程组,则不会等待;
= -1:等待任意一个子进程退出,此时和等价于wait函数;
< -1:等待进程组号为pid绝对值的进程组中的任何子进程退出。
status:存储进程退出时的状态信息。
options:
0:阻塞父进程,等待子进程退出。此时同wait函数。
WNOHANG:若无任何子进程退出,则立即返回。
WUNTRACED:若子进程暂停,则立即返回。(少用)
返回值:
a) 有子进程退出时,waitpid返回已收集到的退出子进程的进程号;
b) 若options设为WNOHANG,调用waitpid时无子进程退出,则返回0;
c) 若调用中出错,返回-1,同时设置errno.
如当pid对应的进程不存在,或pid对应的进程不是调用waitpid进程的子进程,就会出错,此时errno
被设为ECHILD。
*/
waitpid使用示例:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main(int argc, const char* argv[]) {
pid_t pid = -1;
int ret = -1;
int status = 0;
pid = fork();
if (-1 == pid) {
perror("fork");
return 1;
}
if (0 == pid) { // 子进程
printf("子进程%d运行...\n", getpid());
sleep(10);
exit(10); // 子进程终止
}
// 父进程
printf("父进程执行。等待子进程退出,回收其资源...\n");
// 父进程阻塞,等待子进程退出
//ret = waitpid(-1, &status, 0); // 此时等价于wait
ret = waitpid(-1, &status, WNOHANG); // 设置非阻塞
if (-1 == ret) {
perror("wait");
return 1;
}
if (0 == ret) {
printf("暂无子进程退出,waitpid直接返回.\n");
} else {
printf("父进程回收子进程%d的资源...\n", ret);
}
return 0;
}
运行结果:
26行添加sleep(11)后,运行结果:
4. 区分父子进程
通过fork的返回值区分:
若fork成功,则在子进程中返回0,父进程中返回子进程的pid。
示例:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>
int main(int argc, const char* argv[]) {
pid_t pid = -1;
int status = 0;
int ret = -1;
// 创建子进程。若创建成功,则在子进程中返回0,父进程中返回子进程的pid
pid = fork();
if (0 < pid) {
perror("fork");
return 1;
}
if (0 == pid) { // 子进程
printf("这是子进程。进程号 = %d, 父进程号 = %d\n", getpid(), getppid());
exit(0); // 退出子进程,或者return。
} else { // 父进程
printf("这是父进程。进程号 = %d, 子进程号 = %d\n", getpid(), pid);
}
ret = wait(&status); // 父进程等待回收子进程资源
if(-1 == ret) {
perror("wait");
return 1;
}
return 0;
}
运行结果:
5. 父子进程关系
(1)写时拷贝(copy-on-write)。读时共享、写时拷贝。父子进程未修改某变量时,无需拷贝;当父子进程之一修改该变量时,子进程就拷贝一份。
(2)在fork之前、之后open对文件描述信息的影响:
- 在fork前open,父子进程共享一个文件描述信息,包括引用计数、文件偏移等等。子进程复制了父进程的文件表项指针,指向的是同一个文件表项。
- 在fork后open,父子进程各自有自己的文件描述信息,互不影响。一个文件被打开了两次,即引用计数值为2,每个进程都有自己的一份,文件偏移也互不影响。
简单来说,就是先open再fork,文件描述信息是共享的;先fork再open,文件描述信息是独立的。
文件描述信息是内核为每个进程维护的一个文件描述符表,fork时文件描述信息不会被子进程复制,而是被共享(内核空间被所有进程共享)。因此fork之前open、read、write等操作改变了文件偏移,fork之后子进程会从改变了的文件偏移位置继续操作。
6. 父子进程堆区内存开辟释放问题
父子进程都要释放各自堆区的内存。
如下程序父子进程未释放堆区内存,有内存泄漏问题:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main(int argc, const char* argv[]) {
pid_t pid = -1;
int ret = -1;
int status = 0;
int* p = malloc(sizeof(int));
(*p) = 1;
pid = fork();
if (-1 == pid) {
perror("fork");
return 1;
}
if (pid > 0) {
// 父进程
(*p) = 3;
printf("父进程(*p) = %d\n", (*p));
} else {
// 子进程
sleep(1);
printf("子进程(*p) = %d\n", (*p));
exit(0);
}
ret = wait(&status); // 父进程回收子进程资源
if(-1 == ret) {
perror("wait");
return 1;
}
return 0;
}
运行结果:
但存在内存泄漏问题。
valgrind查看内存泄漏情况:
有内存泄漏。
修改:在父子进程退出前释放各自的堆区内存:
free(p);
p = NULL;
再次查看内存泄漏情况:无内存泄漏。
补充: GDB调试多进程
GDB调试
有如下的多进程程序,需要使用GDB分别调试父子进程:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main(int argc, const char* argv[]) {
pid_t pid = -1;
int status = 0;
int ret = -1;
pid = fork();
if (-1 == pid) {
perror("fork");
return 1;
}
if (pid > 0) {
// 父进程
printf("父进程说太强了");
printf("父进程笑尿了");
printf("父进程哈哈哈哈哈");
} else {
// 子进程
printf("子进程说太强了");
printf("子进程笑尿了");
printf("子进程哈哈哈哈哈");
exit(0);
}
ret = wait(&status); // 父进程等待回收子进程资源
if(-1 == ret) {
perror("wait");
return 1;
}
return 0;
}
GDB调试默认跟踪父进程,如下:
如何跟踪子进程呢?
在fork函数调用之前设置跟踪子进程:
set follow-fork-mode child
然后就会跟踪子进程,如下: