1、fork函数
在Linux系统中,fork()函数是用来创建新进程的非常重要的系统调用。通过调用fork(),一个进程可以创建一个几乎完全相同的子进程。fork函数的原型如下:
#include <unistd.h>
pid_t fork(void);
返回值:fork()在父进程和子进程中返回不同的值:
在子进程中,fork()返回0,表示这个进程是子进程。
在父进程中,fork()返回新创建的子进程的PID,用于父进程识别。
如果fork()调用失败,函数返回-1,同时设置全局变量errno来指出具体的错误原因。
fork()函数用于从一个已经存在的进程中创建一个新进程,新进程被称为子进程,而原进程称为父进程。这个过程的核心步骤如下:
1)分配内存和数据结构:内核为新创建的子进程分配独立的内存块和数据结构。
2)复制父进程数据:部分父进程的数据结构会被复制到子进程中,如文件描述符表、信号处理方式等。
3)系统进程表更新:子进程被添加到系统的进程列表中,成为独立的进程。
4)返回值和调度:fork()调用结束后,父进程和子进程分别开始执行,并根据调度器的安排决定谁先运行。
示例代码:
#include <stdio.h>
#include <unistd.h>
int main(void) {
pid_t pid;
printf("Before: pid is %d\n", getpid());
pid = fork(); // 创建子进程
if (pid == -1) {
perror("fork failed");
return 1;
}
printf("After: pid is %d, fork return %d\n", getpid(), pid);
sleep(1); // 延迟1秒
return 0;
}
2、共享资源
在Linux中,fork()系统调用创建的子进程默认会与父进程共享以下资源。
1)文件描述符及文件状态
子进程继承父进程的所有文件描述符副本,并指向相同的内核文件表项,共享文件偏移量、打开模式等属性。例如,子进程修改文件偏移量会影响父进程。
2)进程身份信息
包括实际用户ID、实际组ID、有效用户ID、有效组ID、附加组ID、进程组ID、会话ID等。
3)环境相关设置
当前工作目录和根目录。
文件模式创建屏蔽字(umask)。
控制终端。
4)信号处理配置
子进程继承父进程的信号屏蔽(sigprocmask设置)和信号处理函数,以及打开文件描述符的close-on-exec标志。
5)共享存储段
已通过shmget()或mmap()显式创建的共享内存段会继续共享。
注意:
1)文件共享与偏移量同步:文件描述符指向相同文件表项,因此父子进程对同一文件的读写会互相影响偏移量。
2)内存共享限制:默认内存独立,共享需显式调用API;共享内存需注意同步问题(如通过信号量)。
3)资源释放:共享资源需显式释放,否则可能导致内存泄漏。
示例代码1(文件描述符):
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
int main() {
// 打开一个文件(文件需提前存在,如 test.txt)
int fd = open("test.txt", O_RDWR);
if (fd == -1) {
perror("open");
return 1;
}
// 父进程写入初始内容
write(fd, "Parent writes: ABC\n", 18);
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { // 子进程
// 子进程写入内容
write(fd, "Child writes: XYZ\n", 18);
close(fd); // 子进程关闭文件描述符
_exit(0);
} else { // 父进程
// 父进程等待子进程结束
wait(NULL);
// 父进程读取文件内容
lseek(fd, 0, SEEK_SET); // 重置文件偏移量到开头
char buf[100];
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Parent read content:\n%.*s", (int)n, buf);
}
close(fd); // 父进程关闭文件描述符
}
return 0;
}
说明:
1)父子进程对同一文件的读写会 共享文件偏移量,操作顺序如下:
1.1)父进程写入 ABC(文件偏移量变为 18)。
1.2)子进程写入 XYZ(从偏移量 18 开始,写入后偏移量变为 36)。
1.3)父进程读取时通过 lseek 重置偏移量到 0,读取全部内容。
2)描述符副本独立:子进程关闭自己的 fd 不会影响父进程的 fd
3)close-on-exec 标志
// 设置 close-on-exec 标志(子进程 exec 时会自动关闭 fd)
fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC);
4)竞态条件,如果父子进程不协调操作顺序,可能导致数据混乱。
// 父进程和子进程同时写入(不调用 wait)
if (pid == 0) {
write(fd, "Child writes: XYZ\n", 18);
} else {
write(fd, "Parent writes: DEF\n", 18);
}
输出顺序不确定,可能交替出现 ABC + DEF + XYZ 或 ABC + XYZ + DEF。
如何避免数据混乱,示例代码中通过 wait(NULL) 让父进程等待子进程结束,避免读写竞争。实际开发中可能需要信号量(Semaphore)等同步机制。
示例代码2(共享内存):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#define SHM_SIZE 1024
int main() {
int shmid;
key_t key = 1234; // 共享内存键值
// 创建共享内存段
if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) < 0) {
perror("shmget");
exit(1);
}
// 将共享内存附加到进程地址空间
char *shm_ptr = (char *)shmat(shmid, NULL, 0);
if (shm_ptr == (char *)-1) {
perror("shmat");
exit(1);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程写入数据
printf("Child writing to shared memory...\n");
sprintf(shm_ptr, "Hello from child process!");
shmdt(shm_ptr); // 分离共享内存
exit(0);
} else {
// 父进程等待子进程结束
wait(NULL);
printf("Parent read: %s\n", shm_ptr);
shmdt(shm_ptr); // 分离共享内存
// 删除共享内存段
shmctl(shmid, IPC_RMID, NULL);
}
return 0;
}
注意:
1)竞争条件
上述代码通过 wait(NULL) 让父进程等待子进程结束,避免读写竞争。实际开发中可能需要信号量(Semaphore)等同步机制。
2)共享内存生命周期
共享内存会持续存在直到被显式删除(shmctl(IPC_RMID))或系统重启。
示例代码3(信号):
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void handler(int sig) {
printf("Process %d received signal %d\n", getpid(), sig);
}
int main() {
signal(SIGINT, handler); // 父进程设置信号处理函数
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("Child PID: %d\n", getpid());
while(1); // 等待信号
} else {
// 父进程
printf("Parent PID: %d\n", getpid());
sleep(1);
kill(pid, SIGINT); // 向子进程发送 SIGINT
wait(NULL);
}
return 0;
}
输出结果:
Parent PID: 1234
Child PID: 1235
Process 1235 received signal 2 # 子进程调用 handler
3、共享资源同步
在 Linux 系统中,跨进程同步可通过多种机制实现:信号量、管道、共享内存与原子操作、信号、文件锁、消息队列、套接字。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <signal.h>
#define SHM_SIZE 1024
// 共享内存结构体(存储数据和同步标记)
typedef struct {
volatile sig_atomic_t data_ready; // 原子标记,表示数据是否就绪
char message[SHM_SIZE]; // 共享数据缓冲区
} SharedData;
SharedData *shared_mem; // 全局共享内存指针
// 信号处理函数(子进程接收信号后读取数据)
void child_signal_handler(int sig) {
if (sig == SIGUSR1) {
printf("Child received signal. Reading message: %s\n", shared_mem->message);
shared_mem->data_ready = 0; // 标记数据已读取
}
}
int main() {
int shmid;
key_t key = 1234;
// 创建共享内存
if ((shmid = shmget(key, sizeof(SharedData), IPC_CREAT | 0666)) < 0) {
perror("shmget");
exit(1);
}
// 附加共享内存
shared_mem = (SharedData *)shmat(shmid, NULL, 0);
if (shared_mem == (SharedData *)-1) {
perror("shmat");
exit(1);
}
// 初始化共享内存
shared_mem->data_ready = 0;
sprintf(shared_mem->message, "");
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程:设置信号处理函数
struct sigaction sa;
sa.sa_handler = child_signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
// 子进程等待信号
while (1) {
if (shared_mem->data_ready) {
// 非阻塞检查(实际由信号驱动)
pause(); // 等待父进程发送信号
}
}
} else {
// 父进程:写入数据并通知子进程
printf("Parent writing to shared memory...\n");
sprintf(shared_mem->message, "Hello from parent via signal!");
shared_mem->data_ready = 1; // 标记数据就绪
// 发送信号通知子进程
kill(pid, SIGUSR1);
// 等待子进程处理数据
sleep(1); // 简单同步(实际应用应使用更可靠机制)
// 清理资源
shmdt(shared_mem);
shmctl(shmid, IPC_RMID, NULL);
wait(NULL); // 等待子进程退出
}
return 0;
}
4、不共享资源
1)进程标识信息
包括进程ID(PID)、父进程ID(PPID)、资源使用统计(如tms_utime)等。
2)运行时状态
未决信号(子进程会清空未决信号集)。
父进程设置的定时器或警报(alarm())。
3)独立资源副本
子进程拥有独立的虚拟地址空间、寄存器状态、资源限制(如CPU时间限制)等。