目录
一、进程创建
二、进程API
1. 进程创建 fork()
2. 等待 wait()
3.执行 exec()
3.1 execlp函数
3.2 execl函数
三、其他API
一、进程创建
上文讲述了进程的概念,现在大家对于进程的定义已经有所了解了,本文主要介绍一下进程的基本信息,例如进程是怎么创建和结束的,怎么用操作系统提供的API去自己创建一个进程?
创建:在平时我们打开一个软件只需要双击图标,软件就会自己运行,或者在shell中输入命令就可以创建一个进程来运行程序,那么程序是如何转化为进程的呢?
上图直观的显示出了这个过程,程序运行时首先操作系统会将程序运行需要的数据存入到内存,存入到进程的地址空间中。此时已经将程序的运行代码存到了进程的地址空间中,还需要分配一些堆栈空间用于程序执行,此时就完成了准备工作,最后就是启动程序运行,OS将CPU的控制权转移给进程,从而程序开始执行。
操作系统为每个进程分配独立的地址空间,确保进程之间的内存空间相互隔离,不会相互干扰或访问彼此的数据。,所以我们可以运行多个程序并且他们互不干扰
二、进程API
1. 进程创建 fork()
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
// fork() 出错
fprintf(stderr, "Fork failed\n");
return 1;
}
else if (pid == 0) {
// 子进程
printf("This is the child process\n");
}
else {
// 父进程
printf("This is the parent process\n");
}
return 0;
}
子进程并不是完全拷贝了父进程。具体来说,虽然它拥有自己的地址空间(即拥有自己的私有内存)、寄存器、程序计数器等,但是它从fork()返回的值是不同的。fork()
调用返回两次,一次在父进程中返回子进程的进程 ID(PID),一次在子进程中返回0。通过这种方式,父进程和子进程可以根据返回值来确定自己的身份。
2. 等待 wait()
上述代码如果你多次运行后会发现运行后产生的结果不是固定的,有的情况下父进程会早于子进程被打印出来,这种不确定性是因为CPU调度程序来决定哪个进程先被执行,感兴趣可以了解一下系统调度算法
wait()是一个系统调用(或函数),用于父进程等待其子进程的结束。它允许父进程暂停执行,直到子进程完成执行为止。
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
// fork() 出错
fprintf(stderr, "Fork failed\n");
return 1;
}
else if (pid == 0) {
// 子进程
printf("This is the child process\n");
sleep(2); // 模拟子进程执行一段时间
return 42;
}
else {
// 父进程
printf("This is the parent process\n");
int status;
pid_t child_pid = wait(&status);
if (WIFEXITED(status)) {
int exit_status = WEXITSTATUS(status);
printf("Child process %d exited with status %d\n", child_pid, exit_status);
}
else {
printf("Child process %d did not exit normally\n", child_pid);
}
}
return 0;
}
wait() 函数会阻塞父进程的执行,直到一个子进程结束。在子进程结束后,父进程会继续执行,并从 wait() 调用中返回。
wait() 函数的参数 status 是一个指向整型变量的指针,用于存储子进程的退出状态信息。可以使用宏函数 WIFEXITED(status)、WEXITSTATUS(status) 等来检查和获取子进程的退出状态。
wait() 函数可以等待任意一个子进程结束,也可以使用 waitpid() 函数等待指定的子进程。
3.执行 exec()
给我可执行程序的名称及需要的参数后,exec()会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据)堆、栈及其他内存空间也会被重新初始化。然后操作系统就执行该程序,将参数通过argv传递给该进程。因此,它并没有创建新进程,而是直接将当前运行的程序替换为不同的运行程序
exec 系列的函数有多个变体,包括 execl、execv、execle、execve 等,每个变体在使用上略有不同,但它们的基本目标都是相同的:加载并执行一个新的程序。
我们主要介绍其中的两个:
3.1 execlp函数
成功无返回,失败返回-1。
#include <unistd.h>
int execlp(const char *file, const char *arg0, ..., (char *) NULL);
execlp 函数接受可执行文件的名称(不需要完整路径)和一系列参数,以及一个以 NULL 结束的参数列表。它会在系统的路径中搜索与提供的文件名称匹配的可执行文件,找到后将加载并执行该程序。
3.2 execl函数
成功无返回,失败返回-1。
#include <unistd.h>
int execl(const char *path, const char *arg0, ..., (char *) NULL);
加载一个进程,通过路径和程序名来加载
4.结束进程
- exit() :在程序的任何地方调用 exit() 函数将导致程序立即退出。一般约定,状态码为 0 表示程序正常退出,非零状态码表示程序异常退出或出错。
- 通过信号终止进程:kill <进程ID>
- 强制终止进程: kill -9 <进程ID>
三、其他API
除了上面提到的 fork()、wait()和 exec()之外,还有其他许多与进程交互的方
式。比如可以通过 kill()系统调用向进程发送信号,包括要求进程睡眠、终止或其
他有用的指令。实实上,整个信号子系统提供了一套丰富的向进程传递外部事件的途径。