北京通州马桥网站建设,开工作室做网站怎样找资源,中天建设集团有限公司总部在哪里,专业做网站方案进程#xff08;Process#xff09;是计算机科学中一个基本的概念#xff0c;特别是在操作系统领域中非常重要。它指的是在系统中正在运行的一个程序的实例。每个进程都是系统资源分配的基本单位#xff0c;是程序执行时的一个实例。以下是关于进程的详细解释#xff1a; …进程Process是计算机科学中一个基本的概念特别是在操作系统领域中非常重要。它指的是在系统中正在运行的一个程序的实例。每个进程都是系统资源分配的基本单位是程序执行时的一个实例。以下是关于进程的详细解释
1.进程的基本概念和特征 程序与进程的区别 程序Program是存储在磁盘上的一段可执行的代码是静态的。进程Process是程序在执行过程中的一个实例是动态的。一个程序可以对应多个进程的实例每个实例相互独立运行。 进程的组成 程序代码进程所执行的指令集合。当前状态包括程序计数器、寄存器集合和变量的值。内存空间进程运行时所占用的内存空间包括代码段、数据段、堆栈段等。资源如打开的文件、网络连接等系统资源。 进程的特征 程序实例化进程是一个程序的实例当程序被加载到内存并开始执行时就创建了一个进程。资源拥有每个进程都拥有自己的内存空间、文件描述符、系统指令集、状态等资源。独立性进程之间是相互独立的一个进程的错误不会直接影响其他进程除非它们共享某些资源。并发执行多个进程可以同时存在和执行操作系统通过调度算法来分配CPU时间片给不同的进程。生命周期进程有创建、就绪、运行、阻塞和终止等状态这些状态在操作系统的进程管理中有具体的实现。 4. 进程控制块PCB 每个进程在操作系统中都有一个相应的进程控制块PCB也称为进程描述符。PCB 是操作系统维护的数据结构用来存储和管理进程的各种信息包括但不限于
进程状态例如就绪、运行、阻塞等。进程ID唯一标识符用来区分不同的进程。程序计数器PC指向当前执行的指令地址。内存管理信息包括代码段、数据段、堆栈等内存分配信息。调度信息进程的优先级、调度队列中的位置等。打开文件表进程打开的文件描述符列表。资源使用情况如CPU时间、I/O状态等。父子进程关系父进程ID、子进程ID等。 5. 进程间通信IPC 不同进程之间可以通过操作系统提供的进程间通信IPC机制进行数据交换和协作常见的 IPC 包括
管道Pipe用于具有亲缘关系的进程间通信是半双工的。消息队列Message Queue允许一个进程向另一个进程发送消息的队列。共享内存Shared Memory允许多个进程访问同一块物理内存是最快的 IPC 方法之一。信号量Semaphore用于进程间的同步和互斥。套接字Socket用于不同计算机之间或同一计算机的进程间通信。 6. 进程的创建和销毁
创建进程通常通过系统调用如 fork()、exec() 等来创建新的进程。进程终止进程可以正常退出调用 exit()也可以因为错误或信号而异常终止。操作系统负责回收进程使用的资源释放其占用的内存等。
2.进程创建
1.fork()
用于创建一个新的进程。新进程是调用进程的副本但有自己独立的进程IDPID。
函数原型
#include unistd.h
pid_t fork(void);
返回值
在父进程中fork() 返回新创建的子进程的进程ID一个大于0的数。
在子进程中fork() 返回 0。
如果出现错误则返回 -1。
示例代码
#include stdio.h
#include unistd.h
#include sys/types.hint main() {pid_t pid; // 用于存储 fork() 返回的进程IDprintf(Before fork()\n);pid fork(); // 调用 fork() 创建新的进程if (pid 0) {// 如果 fork() 出现错误fprintf(stderr, Fork failed\n);return 1;} else if (pid 0) {// 子进程代码段printf(Child process: PID %d\n, getpid());printf(Child process: Parent PID %d\n, getppid());// 在子进程中可以执行具体的任务如执行新的程序或者进行其他操作// 这里演示子进程的简单输出} else {// 父进程代码段printf(Parent process: PID %d\n, getpid());printf(Parent process: Child PID %d\n, pid);// 父进程可以继续执行其他任务或者等待子进程结束使用 wait() 或 waitpid()}printf(After fork()\n);return 0;
}实现原理 1. 调用过程
当程序调用 fork() 时操作系统会执行以下步骤 复制父进程 操作系统会创建一个新的进程称为子进程。子进程是父进程的副本包括代码段、数据段、堆栈以及文件描述符表等。 设置进程状态 子进程开始时处于就绪Ready状态等待调度执行。 返回值 在父进程中fork() 返回子进程的PID进程ID。在子进程中fork() 返回0。
2. 内存映像的复制
在调用 fork() 时操作系统需要复制父进程的内存映像。具体复制的内容包括
代码段父进程的可执行代码。数据段包括全局变量和静态变量等。堆栈包括函数调用的信息和局部变量等。
这种复制是通过操作系统的内存管理机制实现的。在实现过程中操作系统通过虚拟内存技术来为子进程分配新的物理内存页并将父进程的对应内存页内容复制到子进程的内存中。这种复制采用了写时复制Copy-on-Write, COW技术即只有在子进程或父进程试图修改内存内容时才会实际进行内存复制操作以节省内存和提高效率。
3. 父子进程的区别
虽然子进程是父进程的副本但它们之间存在一些区别
返回值不同父进程中 fork() 返回子进程的PID而子进程中返回0。进程ID不同父进程和子进程拥有不同的进程ID。父进程的子进程数增加父进程会增加一个子进程。
4. 注意事项
在使用 fork() 时需要注意以下几点 文件描述符的复制子进程会继承父进程的文件描述符但它们操作这些文件描述符时可能会引起意外的影响。因此通常在 fork() 后会调用 exec() 系列函数来替换子进程的地址空间以确保文件描述符处于预期状态。 共享内存父进程和子进程共享相同的内存内容直到其中一个尝试修改共享的内容时操作系统才会复制相关的内存页。
面试题回答两段代码的输出结果是什么
1
#include stdio.h
#include unistd.h
#include sys/types.hint main(int argc, char* argv[]) {for(int i 0; i 3; i) {fork();printf(%da ,i);}return 0;
}2
#include stdio.h
#include unistd.h
#include sys/types.hint main(int argc, char* argv[]) {for(int i 0; i 3; i) {fork();printf(%da\n,i);}return 0;
}
两段代码的输出结果
124个a第一次循环后有两个进程第二次循环后共有4个进程第三次循环后共有8个进程每个进程往printf打印三个a。
214个a第一次循环后共有2个进程打印两个a第二次循环后共有4个进程打印输出4个a第三次循环后共有8个进程打印输出8个a所有共有14个a。 原因在使用 fork() 函数创建子进程时如果父进程有用户态文件缓冲区例如使用 printf() 输出的缓冲区中的数据这些数据也会被复制到子进程的缓冲区中。这会导致在某些情况下出现意外的输出情况或者重复的输出。
详细解释 用户态文件缓冲区 在 C 语言中printf() 等输出函数通常会将数据暂存在用户态文件缓冲区中而不是立即输出到终端。当调用 fork() 创建子进程时父进程的用户态文件缓冲区中的数据也会被复制到子进程的用户态文件缓冲区中包括已经积累的输出数据。 缓冲区复制的影响 如果在调用 fork() 前父进程的缓冲区中有未输出的数据比如部分字符串或者字符这些数据会被完整地复制到子进程的缓冲区中。因此父子进程各自拥有独立的缓冲区副本但它们的初始内容可能是相同的包括未输出的数据部分。 可能的结果 如果父进程在 fork() 前有未输出的部分数据那么每个进程在继续执行时会从自己的缓冲区中输出这些数据。这可能导致输出的重复或者顺序上的不一致。 影响输出的情况 对于带有换行符的 printf(a\n)换行符会触发缓冲区的刷新这会导致输出立即显示在终端上避免父子进程之间的输出混合问题。对于不带换行符的 printf(a)则会把所有的 a 累积在缓冲区中父子进程之间可能会产生交叉的输出。
2.查看进程
1.getpid() 用于获取当前进程的 pid 。 #include sys/types.h
#include unistd.h
pid_t getpid(void);
// 总是成功返回当前进程的pid
2.getppid() getppid() 用于获取父进程的 pid 。 #include sys/types.h
#include unistd.h
pid_t getppid(void);
// 总是成功返回父进程的pid 代码示例 #include stdio.h // 包含标准输入输出库
#include unistd.h // 包含 POSIX 系统服务的头文件int main() {// 使用 getpid() 函数获取当前进程的PIDpid_t pid getpid();pid_t ppid getppid();// 打印当前进程的PIDprintf(PID of this process: %d\n, pid);// 打印父进程的PIDprintf(Parent PID of this process: %d\n, ppid);return 0;
}3.进程终止 1.exit() exit() 是标准 C 库中的函数它用于正常终止一个程序。它执行以下操作 执行 atexit() 注册的函数清理函数。关闭所有标准 I/O 流如文件、套接字等。刷新所有缓冲区。最后调用内核函数 _exit() 终止进程。 void exit(int status);
status 参数指定了进程的退出状态通常用来表示程序的结束状态或者错误码。 示例代码 #include stdio.h
#include stdlib.h
#include unistd.hint main() {printf(Before calling exit()\n);// 注册退出时的清理函数atexit(my_exit_handler);printf(Calling exit()\n);exit(0);// 这里的代码不会被执行printf(After calling exit()\n);return 0;
}// 退出时的清理函数
void my_exit_handler() {printf(Inside exit handler\n);
}2._exit() _exit() 是一个系统调用用于立即终止一个进程不执行任何清理操作直接返回内核。它不会调用 atexit() 注册的清理函数也不会刷新标准 I/O 流。 void _exit(int status);
status 参数指定了进程的退出状态。
示例代码:
#include stdio.h
#include unistd.hint main() {printf(Before calling _exit()\n);_exit(0); // 立即终止进程// 这里的代码不会被执行printf(After calling _exit()\n);return 0;
}区别总结 exit() 标准 C 函数执行标准的清理操作如调用 atexit() 注册的函数。刷新缓冲区关闭文件描述符等。程序员通常使用它来正常终止进程。 _exit() 系统调用直接返回内核不执行任何标准的清理操作。程序员通常用于需要立即退出且不需要执行清理操作的情况。
4.进程回收
进程回收是指当一个进程子进程结束执行时操作系统需要进行的一系列动作包括但不限于
释放进程所占用的内存空间。关闭打开的文件描述符。回收其他系统资源如信号量、消息队列等。通知父进程该子进程的终止状态以便父进程可以做进一步的处理。
进程回收是操作系统管理资源的一部分当一个进程结束时它可能会持有多种资源如内存、文件描述符、系统信号量等。如果这些资源不被及时释放将导致系统资源的浪费甚至可能引发资源耗尽的问题。
孤儿进程Orphan Process 定义 孤儿进程是指其父进程先于它结束或者它的父进程被终止而不能正常等待它的结束状态的进程。当一个进程的父进程结束时操作系统会将这个进程的新父进程设置为 init 进程进程号为 1 的系统进程。 影响 孤儿进程会被 init 进程接管并由 init 进程负责收养和管理。对操作系统的影响较小因为操作系统会确保孤儿进程能够被正确回收不会造成资源泄露或其他问题。
僵尸进程Zombie Process 定义 僵尸进程是指一个已经完成执行的进程但是其父进程还没有调用 wait() 或 waitpid() 等系统调用来获取它的终止状态。僵尸进程会在进程表中保留其进程号和一些基本信息但不再执行任何代码。 影响 僵尸进程占用系统资源如进程表项尽管不占用内存空间但是大量僵尸进程可能导致进程表耗尽进而影响系统的正常运行。过多的僵尸进程可能会导致系统性能下降甚至造成系统崩溃。
1.wait()
wait() 函数阻塞调用进程直到一个子进程结束或者收到一个信号它会暂停当前进程的执行直到有子进程退出为止。如果调用进程没有子进程或者所有子进程都还在运行wait() 会一直阻塞。
函数原型
#include sys/types.h
#include sys/wait.hpid_t wait(int *status);
参数说明
status 是一个指向整型的指针用于存储子进程的退出状态信息。
如果不关心子进程的退出状态可以传入 NULL。
返回值
成功时返回终止的子进程的进程ID。
失败时返回 -1并设置 errno 来指示错误的类型。
子进程退出状态获取
可以通过宏来解析 wait() 返回的 status 变量获取子进程的退出状态信息。
WIFEXITED(status)如果子进程正常终止则为真。
WEXITSTATUS(status)获取子进程的退出状态。
注意事项
如果多个子进程同时结束wait() 只会返回一个已终止子进程的信息如果需要获取所有子进程的终止信息可以循环调用 wait()。
示例代码
#include stdio.h
#include stdlib.h
#include unistd.h
#include sys/types.h
#include sys/wait.hint main() {pid_t pid fork();if (pid 0) {perror(fork failed);exit(EXIT_FAILURE);} else if (pid 0) {// 子进程执行的代码printf(Child process executing...\n);sleep(2); // 模拟子进程执行任务printf(Child process exiting...\n);exit(EXIT_SUCCESS);} else {// 父进程执行的代码printf(Parent process waiting for child...\n);int status;pid_t child_pid wait(status); // 等待子进程结束if (child_pid -1) {perror(wait failed);exit(EXIT_FAILURE);}if (WIFEXITED(status)) {printf(Child process %d exited with status %d\n, child_pid, WEXITSTATUS(status));} else {printf(Child process %d did not exit normally\n, child_pid);}}return 0;
}2.waitpid
waitpid() 函数会阻塞调用进程直到指定的子进程结束或者满足其他的条件。
函数原型
#include sys/types.h
#include sys/wait.hpid_t waitpid(pid_t pid, int *status, int options);
参数说明
pid指定要等待的子进程的进程ID。
如果 pid 0则等待具有指定进程ID的子进程。
如果 pid -1则等待任何子进程。
如果 pid 0则等待和调用进程在同一个进程组的任何子进程。
如果 pid -1则等待属于进程组 -pid 的任何子进程。
status用于存储子进程的退出状态信息的指针。
options控制 waitpid() 的行为的选项
WNOHANG如果没有符合条件的子进程退出则立即返回而不阻塞。
WUNTRACED除了已经退出的子进程外也等待进程状态被暂停的子进程。
返回值
成功时返回终止的子进程的进程ID。
失败时返回 -1并设置 errno 来指示错误的类型。
子进程退出状态获取
可以通过宏来解析 waitpid() 返回的 status 变量获取子进程的退出状态。
WIFEXITED(status)如果子进程正常终止则为真。
WEXITSTATUS(status)获取子进程的退出状态。
示例代码
#include stdio.h
#include stdlib.h
#include unistd.h
#include sys/types.h
#include sys/wait.hint main() {pid_t pid1, pid2, pid;int status;// 创建第一个子进程pid1 fork();if (pid1 0) {perror(fork failed);exit(EXIT_FAILURE);} else if (pid1 0) {// 子进程1执行的代码printf(Child process 1 executing...\n);sleep(2); // 模拟子进程1执行任务printf(Child process 1 exiting...\n);exit(101); // 子进程1退出退出状态为101}// 创建第二个子进程pid2 fork();if (pid2 0) {perror(fork failed);exit(EXIT_FAILURE);} else if (pid2 0) {// 子进程2执行的代码printf(Child process 2 executing...\n);sleep(4); // 模拟子进程2执行任务printf(Child process 2 exiting...\n);exit(202); // 子进程2退出退出状态为202}// 父进程执行的代码printf(Parent process waiting for child processes...\n);// 等待第一个子进程结束,当options设置为0是意思是一直阻塞的类似waitpid waitpid(pid1, status, 0);if (pid -1) {perror(waitpid failed);exit(EXIT_FAILURE);}if (WIFEXITED(status)) {printf(Child process %d exited with status %d\n, pid, WEXITSTATUS(status));} else {printf(Child process %d did not exit normally\n, pid);}// 等待第二个子进程结束使用WNOHANG选项非阻塞printf(Parent process checking child 2...\n);pid waitpid(pid2, status, WNOHANG);if (pid 0) {printf(Child process 2 is still running\n);} else if (pid -1) {perror(waitpid failed);exit(EXIT_FAILURE);} else {if (WIFEXITED(status)) {printf(Child process %d exited with status %d\n, pid, WEXITSTATUS(status));} else {printf(Child process %d did not exit normally\n, pid);}}// 等待所有子进程结束使用WUNTRACED选项printf(Parent process waiting for all children...\n);while ((pid waitpid(-1, status, WUNTRACED)) 0) {if (WIFEXITED(status)) {printf(Child process %d exited with status %d\n, pid, WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {printf(Child process %d terminated by signal %d\n, pid, WTERMSIG(status));} else if (WIFSTOPPED(status)) {printf(Child process %d stopped by signal %d\n, pid, WSTOPSIG(status));}}if (pid -1) {perror(waitpid failed);exit(EXIT_FAILURE);}return 0;
}5.进程执行
exec() 系列系统调用函数用于在当前进程中执行新的程序。这些函数会用指定的程序替换当前进程的内存空间从而执行新程序。在Linux系统编程中exec() 函数族包括多个变种如 execve()、execl()、execv() 等它们允许以不同的方式传递命令行参数、环境变量和工作目录。
1.execve()
execve() 系统调用函数是 exec() 系列函数中最灵活和通用的一个它允许在当前进程中执行新程序并且可以指定命令行参数和环境变量。
#include unistd.hint execve(const char *filename, char *const argv[], char *const envp[]);
execve() 函数会用指定的程序替换当前进程的内容执行新程序。
参数说明
filename要执行的新程序的路径。
argv一个以 NULL 结尾的字符串数组用于传递给新程序的命令行参数。
envp一个以 NULL 结尾的字符串数组用于传递给新程序的环境变量。
返回值
如果执行成功execve() 函数不会返回因为当前进程的内容已经被替换为新程序的内容。
如果执行失败返回 -1 并设置 errno 来指示错误的类型。
示例代码
#include stdio.h
#include stdlib.h
#include unistd.hint main() {pid_t pid;// 创建子进程pid fork();if (pid 0) {perror(fork failed);exit(EXIT_FAILURE);} else if (pid 0) {// 子进程执行的代码char *args[] {./child, Hello from child!, NULL};char *envp[] {PATH/bin:/usr/bin, HOME/home/user, NULL};printf(Child process executing...\n);// 执行新程序 ./childif (execve(./child, args, envp) -1) {perror(execve failed);exit(EXIT_FAILURE);}// 如果 execve() 成功子进程不会继续执行到这里printf(This line will not be executed in child process.\n);} else {// 父进程执行的代码printf(Parent process waiting for child...\n);wait(NULL); // 等待子进程结束printf(Child process finished.\n);}return 0;
}// child.c#include stdio.hint main(int argc, char *argv[]) {if (argc ! 2) {fprintf(stderr, Usage: %s message\n, argv[0]);return 1;}printf(Child process received message: %s\n, argv[1]);return 0;
}
2.execl()
execl() 系统调用函数用于在当前进程中执行新的程序它是 exec() 系列函数中的一员专门用于接受可变长度的参数列表。
函数原型
#include unistd.h
int execl(const char *path, const char *arg0, ... /* (char *) NULL */);
execl() 函数会用指定的程序替换当前进程的内容执行新程序。
path 是要执行的程序的路径。
arg0, ... 是一个可变长度的参数列表以 NULL 结尾用于传递命令行参数给新程序。
参数说明
path要执行的新程序的路径。
arg0, ...命令行参数列表以及可选的 (char *) NULL 作为结束标志。
返回值
如果执行成功execl() 函数不会返回因为当前进程的内容已经被替换为新程序的内容。
如果执行失败返回 -1 并设置 errno 来指示错误的类型。
示例代码
#include stdio.h
#include stdlib.h
#include unistd.h
#include sys/wait.hint main() {pid_t pid;// 创建子进程pid fork();if (pid 0) {perror(fork failed);exit(EXIT_FAILURE);} else if (pid 0) {// 子进程执行的代码printf(Child process executing...\n);// 使用 execl 执行新程序 lsif (execl(/bin/ls, ls, -l, NULL) -1) {perror(execl failed);exit(EXIT_FAILURE);}// 如果 execl() 成功子进程不会继续执行到这里printf(This line will not be executed in child process.\n);} else {// 父进程执行的代码printf(Parent process waiting for child...\n);wait(NULL); // 等待子进程结束printf(Child process finished.\n);}return 0;
}3.execlp()
execlp() 系统调用函数与 execl() 函数非常类似主要区别在于 execlp() 允许不用指定程序的完整路径而是根据系统的 PATH 环境变量来查找可执行文件。
函数原型
#include unistd.h
int execlp(const char *file, const char *arg0, ... /* (char *) NULL */);
execlp() 函数在当前进程中执行指定的程序 file。
file 是要执行的程序的名称可以是不带路径的可执行文件名称。
arg0, ... 是一个可变长度的参数列表用于传递命令行参数给新程序以 NULL 结尾。
参数说明
file要执行的程序的名称可以是不带路径的可执行文件名。
arg0, ...命令行参数列表以及可选的 (char *) NULL 作为结束标志。
返回值
如果执行成功execlp() 函数不会返回因为当前进程的内容已经被替换为新程序的内容。
如果执行失败返回 -1 并设置 errno 来指示错误的类型。
示例代码
#include stdio.h
#include stdlib.h
#include unistd.h
#include sys/wait.hint main() {pid_t pid;// 创建子进程pid fork();if (pid 0) {perror(fork failed);exit(EXIT_FAILURE);} else if (pid 0) {// 子进程执行的代码printf(Child process executing...\n);// 使用 execlp 执行 ls 命令if (execlp(ls, ls, -l, NULL) -1) {perror(execlp failed);exit(EXIT_FAILURE);}// 如果 execlp() 成功子进程不会继续执行到这里printf(This line will not be executed in child process.\n);} else {// 父进程执行的代码printf(Parent process waiting for child...\n);wait(NULL); // 等待子进程结束printf(Child process finished.\n);}return 0;
}4.exec函数族的实现原理
exec() 函数族包括 execve()、execl()、execlp() 等实现的原理涉及操作系统的进程管理和内存管理机制。这些函数族的共同目标是在当前进程中执行一个新的程序取代当前进程的内存映像。exec族函数的实现原理图如execlp(“ls”, “ls”, “-l”, NULL); 原理概述 进程内存结构 操作系统为每个进程分配一块内存空间用于存储程序的代码、数据和堆栈等信息。这块内存空间包括了程序代码段、数据段、堆、栈等区域。 进程执行过程 当调用 exec() 函数族中的某一个函数时操作系统首先会将新程序的可执行文件加载到当前进程的内存空间中。新程序的可执行文件包含了程序的代码段、数据段以及其他必要的资源信息。 内存映像替换 exec() 函数族中的函数会将当前进程的整个内存映像替换为新程序的内存映像。这包括清除当前进程的代码、数据和堆栈等区域并将新程序的相应部分加载到这些区域。因此调用 exec() 函数后原有进程的代码、数据和堆栈等内容都会被新程序的内容取代。 文件描述符的处理 在 exec() 执行过程中文件描述符如打开的文件、网络连接等会保留。这意味着新程序可以继续使用原有进程打开的文件或者其他资源而无需重新打开。 进程控制流 调用 exec() 函数后原有进程的执行流会停止不会继续执行 exec() 调用之后的代码。因为当前进程的内存映像已经被完全替换为新程序的内容。如果 exec() 函数调用失败原有进程会继续执行不会发生替换操作。 错误处理 如果 exec() 函数调用失败返回 -1通常会设置全局变量 errno 来指示具体的错误类型比如文件不存在、权限不足等。
总结
exec函数是用一个新程序替换了当前进程的代码段、数据段、堆和栈原有的进程空间没有发生变化并没有创建新的进程进程PID没有发生变化。