1. 进程创建与管理
在C语言中,进程的创建和管理是通过几个关键系统调用完成的。最常见的进程管理函数有 fork()
, execvp()
,以及 wait()
。我们来逐一分析它们。
fork()
— 进程的复制
fork()
是用来创建新进程的系统调用。它会复制当前的进程(父进程),创建一个几乎完全相同的子进程。子进程从 fork()
调用之后继续执行,但有一个返回值区分父子进程:
- 父进程返回子进程的PID(进程ID)。
- 子进程返回0。
使用 fork()
时,父进程和子进程会在内存中各自运行,互不干扰。
execvp()
— 执行新程序
execvp()
用于在当前进程的空间中加载并执行一个新程序。与 fork()
创建的新进程不同,execvp()
会替换当前进程的内存空间,加载一个新的程序执行。因此,execvp()
之后的代码将不再执行当前程序,而是执行新的程序。
例如,以下代码:
char *arg[] = {"ls", "-l", "/", NULL};
execvp(arg[0], arg);
执行了 ls -l /
命令,execvp()
会将当前进程替换成 ls
命令并执行。
2. 进程的退出
进程退出是操作系统中非常重要的一个环节。进程的退出有两种主要方式:正常退出和异常退出。
正常退出
-
exit()
:此函数用于正常退出进程,并可以传递一个退出状态值。这些退出状态值通常用于通知父进程该子进程的退出情况。void exit(int status);
-
_exit()
:这个函数和exit()
类似,但不执行标准I/O缓冲区的清理工作,因此速度较快。通常在fork()
后的子进程中使用_exit()
来直接退出。 -
进程正常退出有三种常见方式:
- 从
main()
函数返回。 - 使用
exit()
。 - 使用
_exit()
。
- 从
异常退出
异常退出指进程由于某些错误或信号终止,例如:
- 被某个信号终止(如
kill
或abort
)。 - 发生未捕获的异常。
其中 abort()
函数会生成一个信号,通常会导致程序异常退出。
3. wait()
的使用
在多进程的程序中,父进程可能需要等待子进程的执行结果,或者等待子进程结束以清理资源。wait()
函数就可以实现这一功能。
wait()
函数
wait()
函数用于阻塞父进程,直到一个子进程结束。它还允许父进程获取子进程的退出状态。
pid_t wait(int *wstatus);
wstatus
:指向一个整数变量,用于存储子进程的退出状态。如果wstatus
为NULL
,父进程将不关心子进程的退出状态。- 返回值:返回已退出的子进程的PID。如果没有子进程,返回
-1
。
例子:
pid_t pid = wait(NULL); // 等待任一子进程退出,不关心状态
- 如果没有子进程,调用
wait()
会失败,返回-1
,并设置errno
为ECHILD
,表示没有子进程。
为什么使用 wait()
?
wait()
的主要作用是回收子进程资源,防止出现“僵尸进程”。如果父进程不调用 wait()
,子进程退出时,它的进程表项仍然保留在操作系统中,直到父进程明确回收它。这样的进程称为僵尸进程。
4. 文件操作与同步
在多进程环境中,若多个进程共享同一个文件进行写操作,必须考虑到进程间的同步问题,避免出现竞争条件。我们可以通过以下步骤来实现:
- 父进程和子进程分别打开同一个文件进行写入。
- 每个进程写入10条数据,标明它们分别是由不同进程写入的。
- 父进程在子进程写完数据后,进行收尸操作,回收子进程资源,防止僵尸进程。
程序实现
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>void write_to_file(const char *filename, const char *text) {int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0644);if (fd == -1) {perror("Failed to open file");exit(1);}write(fd, text, strlen(text));close(fd);
}int main() {const char *filename = "output.txt";pid_t pid;for (int i = 0; i < 2; i++) {pid = fork();if (pid == 0) {// 子进程for (int j = 0; j < 10; j++) {char text[100];snprintf(text, sizeof(text), "Child %d writes line %d\n", getpid(), j + 1);write_to_file(filename, text);}_exit(0); // 使用 _exit() 防止调用 exit() 时引发资源清理} else if (pid < 0) {perror("Fork failed");exit(1);}}// 父进程等待子进程结束for (int i = 0; i < 2; i++) {wait(NULL); // 收尸}printf("Parent process is done.\n");return 0;
}
查看全部
程序说明:
- 文件操作:父进程和子进程分别使用
open()
打开文件并进行写操作。写操作使用O_APPEND
标志,这样每次写入的数据都会被追加到文件末尾。 fork()
创建两个子进程:每个子进程都会写入10行数据。wait()
阻塞父进程:父进程在两个子进程都结束后再退出,确保在父进程退出之前回收所有子进程资源,避免僵尸进程。
5. 总结
通过本文的学习,我们深入了解了C语言中的进程管理,包括 fork()
、execvp()
、wait()
等函数的使用。通过这些机制,我们可以实现父子进程的创建、程序的执行以及资源的回收。
在实际编程中,fork()
和 exec()
是两个非常常用的系统调用,父进程通过 fork()
创建子进程,而子进程通过 exec()
执行新的程序。在多进程环境下,我们还必须关注僵尸进程的管理,确保父进程使用 wait()
正确地回收子进程资源。
文件操作中的同步也是一个重要课题,当多个进程访问同一文件时,我们必须小心同步问题,避免数据竞争。