进程的创建与调度——fork,exec,system等系统调用/函数
什么是进程?
要弄清楚什么是进程,我们要先理解什么是程序,什么是代码。代码顾名思义,大家都知道,我们平时所敲出来的那一堆文本就是代码,那程序呢,程序是**代码经过编译/链接后所形成的可执行文件,程序所在的位置也是至关重要,程序存在于磁盘上,是一个静态文件。**接下来,我们就可以知道进程是什么了:
**进程是程序被加载到内存并开始运行后的"实例"。**我们只需要知道,程序本来在磁盘上,如果我们把他加载到内存上运行起来,那么这就是个进程。
进程是操作系统进行资源分配和调度的基本单位。
1 进程处理相关函数介绍
首先明确一些概念:
(1)pid:pid全称是process id,指的是进程的id;
(2)子进程与父进程:由父进程创建的进程叫做子进程,一般情况下子进程继承父进程的"运行环境",但拥有独立的执行生命周期;
1.1 获取进程id
**pid_t getpid(void):**获取当前进程id;
**pid_t getppid(void):**获取父进程id;
每个进程都有一个独立的pid。
1.2 创建进程
最常见的创建进程的方式fork;
pid_t fork(void):复制当前进程,创建一个子进程;
返回值(重点):
父进程:返回子进程pid;
子进程:返回0;
失败返回-1;
返回值部分看代码应该更清晰一点。

接下来我们讲execve()函数,很多人会把execv跟fork弄混。
**fork:**创建一个新的进程(子进程);
**execv:**保持原进程不变,只是运行一个新的程序,但还是在原进程跑的这个程序,也就是execv之后pid不变;
int execv(const char *pathname, char *const argv[], char *const envp[]);
参数:
**const char* pathname:**要运行的新的程序的绝对路径(可执行文件)(execve 不会搜索 PATH);
char* const argv[]: 给新程序main函数的参数,一个"以 NULL 结尾的字符串指针数组";
char* const envp[]: 环境变量数组,一个"以 NULL 结尾的字符串指针数组";
**返回值:**成功则没有返回值,因为已经去运行另一个程序了,存在返回值也没有意义,失败返回-1;
综上所述,fork用于创建一个子进程,在内存中开辟新的空间,execv则是保持pid不变,而执行另一个程序,所以,这也就产生了Linux系统编程中**fork() + exec() 的经典组合**,fork()专职于复制进程,execv()专职于加载程序。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
printf("现在fork进程\n");
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
return 0;
}else if(pid == 0)
{
printf("这是子进程%d......\n",getpid());
char* argvs[] = {"/bin/ls","-a",NULL};
int result = execv(argvs[0],argvs);
if(result < 0)
{
perror("execv");
return 0;
}
}else{
printf("这是父进程%d......\n",getpid());
waitpid(pid,NULL,0);
}
return 0;
}
除了fork()与execv()之外,还有一种常见的创建进程的方法是**system();system()**是一个标准 C 库函数,它提供了一个非常简单的方式来执行一个shell 命令。
system功能:
-
启动一个 shell(通常是/bin/sh),类似于我们在终端中输入一个bash;
-
然后由这个 shell 来执行你提供的命令字符串。并且函数会阻塞,直到命令执行完成。
int system(const char *command);
**参数 const char *command :**一个指向以NULL结尾的字符串的指针,这个字符串就是你想要执行的 shell 命令。例如"ls-a"等;
system缺点:
1.性能开销:起一个 shell(fork/exec)很重;
2.线程相关:POSIX 明确说system()不要求线程安全(多线程程序里更要谨慎);
3.凡是涉及外部输入、网络数据、配置文件内容、环境变量等,尽量不要用system();改用fork+execv;
1.3进程退出
进程退出的方法有:exit()/_exit()/return
void _exit(int status):_exit是系统调用,用于立即终止一个进程,定义在uinstd.h中。这个调用确保进程立即退出,不执行任何清理操作。一般用于子进程终止,防止子进程影响父进程。
void exit(int status):exit()由c标准库函数提供,定义在stdlib.h中,作用是终止当前进程,但在此之前会执行三种清理操作。一般在父进程使用exit();
系统调用不进行清理,库函数exit进行清理操作;
清理操作具体指:**用户态清理,**刷新 stdio 缓冲、调用atexit注册函数等,然后再进入内核结束进程。
1.4等待与回收进程
进程运行结束是需要回收的,子进程是需要其父进程帮忙回收的,子进程退出后,如果父进程不回收,会留下 僵尸进程(Z)。所以父进程一般会等待子进程运行结束后,帮忙回收,然后再结束自己的进程。
故而会存在一些父进程等待子进程的函数如wait/waitpid等
pid_t wait(int *wstatus):等待任意一个子进程结束并回收;成功返回回收的子进程pid,失败返回-1;
**pid_t waitpid(pid_t pid, int *wstatus, int options):**等待指定的进程;
参数:
pid_t pid:等待的子进程id;为-1时则等待任意子进程;
int options:0:阻塞等待,WHOHANG:非阻塞等待;
2 进程分类
在这一章节我们将介绍僵尸进程,孤儿进程,守护进程的相关内容。
2.1 僵尸进程
子进程已经退出了,但它的父进程还没 wait 回收它的退出状态,此时,该进程被称为"僵尸进程"。
僵尸进程的特点:
-
进程已经死亡,不再运行,不占用任何 CPU 和内存资源。
-
但在内核的进程表中仍然保留着一个task_struct条目,记录着进程的 PID、退出状态、资源使用统计等信息。
-
它仍然占用着一个 PID。如果系统中存在大量僵尸进程,可能会耗尽可用的 PID,导致新进程无法创建。
查看僵尸进程:
ps aux | grep Z
怎么处理
-
根治:父进程调用wait/waitpid回收;
-
异步处理:父进程捕捉SIGCHLD,在 handler 里循环waitpid(-1,…WNOHANG) ;
-
杀死父进程:僵尸本身杀不掉(它已"死"),要杀的是父进程或让父进程修复/重启,僵尸会被回收。
2.2 孤儿进程
父进程先退出了,子进程还在运行,这个子进程就成了"孤儿"。孤儿进程(子进程)的PPID会被内核改为 **1**(或被 systemd 的"子收割者"接管);
孤儿进程本身不是问题,一般不会造成危害,Linux 会把它"托管"给 1 号进程(`init/systemd` 或 subreaper)。它结束时会被新的父进程wait回收,所以**一般不会变成僵尸长期堆积**。
2.3 守护进程
一种长期在后台运行的服务进程,其生命周期很长,通常在系统启动时启动,在系统关闭时终止通常:
-
不依赖终端(脱离控制终端)
-
在后台运行
-
管理自身日志、PID、工作目录、权限等
创建守护进程:
// daemon_min.c
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
static void daemonize(const char *log_path)
{
pid_t pid;
// 1) fork,让父进程退出
pid = fork();
if (pid < 0) { perror("fork"); exit(1); }
if (pid > 0) exit(0);
// 2) 子进程创建新会话,脱离控制终端
if (setsid() < 0) { perror("setsid"); exit(1); }
// 3) 可选:忽略 SIGHUP(防止终端挂断影响)
signal(SIGHUP, SIG_IGN);
// 4) 二次 fork,避免重新获得控制终端
pid = fork();
if (pid < 0) { perror("fork2"); exit(1); }
if (pid > 0) exit(0);
// 5) 清 umask、切工作目录
umask(0);
if (chdir("/") < 0) { perror("chdir"); exit(1); }
// 6) 关闭继承的文件描述符(尽量彻底)
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
for (int fd = 0; fd < (int)rl.rlim_max; fd++) close(fd);
}
// 7) 重定向 stdin -> /dev/null
int fd0 = open("/dev/null", O_RDONLY);
if (fd0 < 0) exit(1);
if (dup2(fd0, STDIN_FILENO) < 0) exit(1);
// 8) 重定向 stdout/stderr -> log 文件(append)
int fdlog = open(log_path, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fdlog < 0) exit(1);
if (dup2(fdlog, STDOUT_FILENO) < 0) exit(1);
if (dup2(fdlog, STDERR_FILENO) < 0) exit(1);
// 这些 fd 已经 dup2 到 0/1/2 了,可以关掉原始 fd
if (fd0 > 2) close(fd0);
if (fdlog > 2) close(fdlog);
// 让 stdio 立即刷新(日志更"实时")
setvbuf(stdout, NULL, _IOLBF, 0); // 行缓冲
setvbuf(stderr, NULL, _IONBF, 0); // 无缓冲
}
int main(void)
{
daemonize("/tmp/mydaemon.log");
// 这里开始就是守护进程逻辑
while (1) {
time_t t = time(NULL);
fprintf(stdout, "daemon alive: %ld\n", (long)t);
fprintf(stderr, "stderr example: errno=%d\n", errno);
sleep(5);
}
return 0;
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)