一,进程的基本认识
1,进程的简介
进程描述是一个程序执行过程。当程序执行后,执行过程开始,则进程产生;执行过程结束,则进程也就结束了.进程和我们普通电费程序最大的区别就是,进程是动态的,他是一个过程,而程序是静态的.
2,进程的定义
在 C 语言中,进程通常被认为是一个正在执行的程序的实例。
从操作系统的角度来看,进程包含了程序的代码、数据、堆、栈以及各种系统资源(如文件描述符、信号处理等)。每个进程都在自己独立的内存空间中运行,与其他进程相互隔离,以确保安全性和稳定性。
在 C 语言编程中,可以通过系统调用和库函数来操作进程。例如,可以使用 fork
系统调用创建一个新的进程,新创建的子进程会继承父进程的一部分资源,但拥有独立的内存空间和运行状态。
3,进程和程序的区别
一、定义
- 程序:是一组指令的有序集合,是一个静态的概念。它以文件的形式存储在磁盘等存储介质上,如一个 C 语言源程序经过编译链接后生成的可执行文件。
- 进程:是程序的一次执行过程,是一个动态的概念。它包含了程序执行所需的各种资源,如内存空间、寄存器状态、文件描述符等。
二、特征
- 程序:
- 具有永久性,只要不被删除或损坏,程序会一直存在于存储介质中。
- 是被动的实体,本身不能运行,只有被加载到内存中并被执行时才成为进程。
- 进程:
- 具有动态性,其状态会随着时间不断变化,如从创建到运行、暂停、终止等不同状态的转换。
- 是活动的实体,在系统中可以独立运行,并且可以和其他进程并发执行。
三、资源占用
- 程序:不占用系统的运行资源,如 CPU、内存等,只是存储在磁盘上的代码和数据。
- 进程:在运行时需要占用系统资源,包括 CPU 时间片、内存空间、I/O 设备等。每个进程都有自己独立的地址空间,确保进程之间的数据隔离。
四、组成部分
- 程序:由代码和数据组成,代码是一系列指令,数据包括常量、变量等。
- 进程:由程序代码、数据、进程控制块(PCB)等组成。PCB 包含了进程的各种状态信息、资源分配情况、调度信息等,是操作系统管理进程的重要依据。
例如,一个用 C 语言编写的文本编辑器程序,当它存储在磁盘上时,它只是一个程序。只有当这个程序被加载到内存中执行时,才成为一个进程。在运行过程中,进程会占用 CPU 时间进行文本编辑操作,使用内存来存储正在编辑的文本内容等数据。如果有多个用户同时运行这个文本编辑器程序,那么系统中会有多个不同的进程,每个进程都有自己独立的内存空间和运行状态,但它们都在执行相同的程序代码。
4, C语言的并发和并行.
在学习进程和线程的主要目的就是并行和并发编程,所以了解这类知识是很重要的.


5,进程管理
二,进程的空间分配和堆栈大小
1,进程的空间分配

用户空间又具体分为如下区间
2,虚拟地址与物理地址

3,进程的堆栈大小

三,进程的状态管理
1、进程的主要状态
-
就绪状态(Ready):
- 进程已准备好运行,等待被操作系统调度分配 CPU 时间片。
- 此时进程的所有资源都已准备好,只等 CPU 可用。
- 例如,一个在等待队列中的进程,随时可以被调度执行。
-
运行状态(Running):
- 进程正在被 CPU 执行。
- 处于这个状态的进程占用 CPU 资源,执行其指令。
- 例如,正在进行计算或处理任务的进程。
-
阻塞状态(Blocked):
- 进程由于等待某个事件(如 I/O 操作完成、等待信号等)而暂停执行。
- 此时进程不能继续执行,直到等待的事件发生。
- 例如,一个进程正在等待用户输入或者等待从磁盘读取数据。
2、状态转换
-
就绪 -> 运行:
- 当操作系统选择一个就绪进程并分配 CPU 时间片给它时,该进程从就绪状态转换为运行状态。
- 例如,操作系统的调度程序从就绪队列中选择一个进程,并将其加载到 CPU 上执行。
-
运行 -> 就绪:
- 当正在运行的进程时间片用完或者被更高优先级的进程抢占时,它会从运行状态转换回就绪状态,重新等待被调度。
- 例如,一个进程的时间片到期,操作系统将其从 CPU 上移除,放入就绪队列。
-
运行 -> 阻塞:
- 如果正在运行的进程需要等待某个事件发生,它会从运行状态转换为阻塞状态。
- 例如,一个进程发起一个磁盘 I/O 操作,此时它会进入阻塞状态,等待 I/O 完成。
-
阻塞 -> 就绪:
- 当进程等待的事件发生时,它会从阻塞状态转换为就绪状态,等待被调度执行。
- 例如,一个进程等待的磁盘 I/O 操作完成,操作系统将其状态改为就绪,放入就绪队列。
3,通过用户输入来理解进程状态的变化
#include <stdio.h>
int main()
{
int num=-1;
printf("please input number:");
scanf("%d",&num);
printf("num=%d\n",num);
return 0;
}
状态的变化过程:
四,进程的相关命令
一、查看进程信息
-
ps
:- 功能:报告当前系统的进程状态。
- 常用参数:
-e
:显示所有进程。-f
:全格式显示,包括 UID、PID、PPID、C、STIME、TTY、TIME、CMD 等信息。
- 示例:
ps -ef
将显示系统中所有进程的详细信息。
-
top
:- 功能:实时显示系统中各个进程的资源占用情况,类似于 Windows 系统中的任务管理器。
- 可以动态地查看 CPU、内存等资源的使用情况,并可以按照不同的字段进行排序。
二、终止进程
-
kill
:- 功能:向进程发送信号,以控制进程的行为。最常用的是发送终止信号(SIGTERM)来终止进程。
- 用法:
kill [信号编号] 进程ID
。例如,kill -9 1234
表示向进程 ID 为 1234 的进程发送强制终止信号(SIGKILL)。
-
killall
:- 功能:通过进程名称来终止进程。
- 用法:
killall [进程名称]
。例如,killall firefox
将终止所有名为 firefox 的进程。
三、启动和停止进程
-
bg
:- 功能:将一个在前台运行的进程放到后台运行,并继续执行。
- 用法:在前台运行的进程中按下
Ctrl+Z
暂停进程,然后输入bg
将其放到后台。
-
fg
:- 功能:将一个在后台运行的进程调到前台运行。
- 用法:
fg [作业编号]
。如果只有一个后台作业,可以直接输入fg
将其调到前台。
四、查看进程树
pstree
:- 功能:以树状结构显示系统中的进程关系。
- 常用参数:
-p
:显示进程的 PID。-u
:显示进程的所属用户。
- 示例:
pstree -p
将以树状结构显示系统中所有进程的 PID。
五、进程优先级调整
-
nice
:- 功能:在启动进程时设置进程的优先级。优先级的值越低,进程的优先级越高。
- 用法:
nice -n [优先级值] [命令]
。例如,nice -n -10 firefox
将以较高的优先级启动 firefox 浏览器。
-
renice
:- 功能:调整已经运行的进程的优先级。
- 用法:
renice [优先级值] [进程ID]
。例如,renice -5 1234
将调整进程 ID 为 1234 的进程的优先级为 -5。
这些命令在 Linux 系统中非常有用,可以帮助用户管理和监控系统中的进程。
五,进程的基本使用
1,创建进程
在 Unix/Linux 系统中,可以使用fork()
函数来创建新进程。
-
fork()
函数介绍:fork()
函数会创建一个新的进程,这个新进程几乎是当前进程的一个副本,包括代码、数据和打开的文件描述符等。fork()
函数返回两次,一次在父进程中,返回新创建子进程的进程 ID;一次在子进程中,返回 0。- 如果
fork()
失败,会返回 -1。
-
#include <stdio.h> #include <unistd.h> int main() { pid_t pid; pid = fork(); if (pid == 0) { // 子进程 printf("This is child process.\n"); } else if (pid > 0) { // 父进程 printf("This is parent process. Child process ID is %d.\n", pid); } else { // fork 失败 perror("fork"); return -1; } return 0; }
2,进程多任务
一、进程多任务的概念
进程多任务是指操作系统能够同时管理多个进程,让它们在不同的时间段内共享系统资源,如 CPU、内存、I/O 设备等。每个进程都被认为是独立的执行单元,拥有自己的地址空间、数据和代码。通过快速切换不同进程的执行,操作系统给用户一种多个任务同时进行的错觉。
二、实现进程多任务的方式
-
时间片轮转调度
- 操作系统将 CPU 的时间划分为固定长度的时间片。
- 每个进程在被分配到一个时间片内执行,如果时间片用完,操作系统会暂停该进程的执行,并切换到另一个进程。
- 这样,多个进程可以轮流使用 CPU,从而实现多任务。
-
优先级调度
- 每个进程被赋予一个优先级。
- 操作系统优先执行优先级高的进程,当高优先级进程执行完毕或进入等待状态时,再执行低优先级的进程。
- 这种方式可以确保重要的任务能够及时得到处理。
三、进程多任务的优点
-
提高系统资源利用率
- 多个进程可以同时使用 CPU、内存和 I/O 设备等资源,避免了资源的闲置浪费。
- 例如,当一个进程在等待 I/O 操作完成时,CPU 可以被分配给其他进程使用。
-
增强系统的响应性
- 用户可以同时运行多个应用程序,每个程序都能及时得到响应。
- 即使某个进程占用了大量的 CPU 时间,其他进程也不会被完全阻塞,系统仍然能够保持一定的响应能力。
-
实现并行处理
- 在多核处理器系统中,进程多任务可以充分利用多个 CPU 核心,实现真正的并行处理。
- 不同的进程可以被分配到不同的核心上同时执行,从而大大提高系统的处理能力。
四、进程多任务的挑战
-
进程切换开销
- 频繁地进行进程切换会带来一定的开销,包括保存和恢复进程的上下文、更新调度数据结构等。
- 这些开销可能会影响系统的性能,特别是在进程数量较多或切换频率较高的情况下。
-
资源竞争和同步问题
- 多个进程同时访问共享资源时,可能会出现资源竞争和冲突。
- 为了解决这些问题,需要使用同步机制,如互斥锁、信号量等,但这些机制也会增加系统的复杂性和开销。
-
内存管理问题
- 每个进程都需要一定的内存空间来存储代码、数据和栈等。
- 当系统中同时运行的进程数量较多时,内存可能会成为瓶颈,需要有效的内存管理策略来确保系统的稳定性和性能。
总之,进程多任务是现代操作系统的重要特征之一,它为用户提供了更加高效和便捷的计算环境。然而,在实现进程多任务的过程中,也需要解决一系列的技术挑战,以确保系统的性能和稳定性。
五、在 Unix/Linux 系统下,可以使用fork()
函数创建多个进程来实现多任务。
#include <stdio.h>
#include <unistd.h>
void task1() {
for (int i = 0; i < 5; i++) {
printf("Task 1: %d\n", i);
sleep(1);
}
}
void task2() {
for (int i = 0; i < 5; i++) {
printf("Task 2: %d\n", i);
sleep(1);
}
}
int main() {
pid_t pid;
pid = fork();
if (pid == 0) {
// 子进程执行 task1
task1();
} else if (pid > 0) {
// 父进程执行 task2
task2();
} else {
perror("fork");
return -1;
}
return 0;
}
3进程的退出
一、正常退出
return
语句:- 在
main
函数中使用return
语句可以使进程正常退出。return
语句的返回值通常被用作进程的退出状态码。 - 例如:
- 在
int main() {
// 一些操作
return 0; // 0 通常表示正常退出
}
exit
函数:exit
函数可以在程序的任何地方调用,用于立即终止进程的执行。exit
函数接受一个整数参数作为进程的退出状态码。- 例如:
#include <stdlib.h>
void someFunction() {
// 一些操作
exit(0); // 0 表示正常退出
}
二、异常退出
-
发生严重错误:
- 当程序遇到严重错误,如内存访问错误、除零错误等,可能会导致进程异常退出。
- 这种情况下,操作系统通常会生成错误信息并终止进程。
-
接收到信号:
- 进程可以接收到来自操作系统或其他进程发送的信号。某些信号会导致进程异常退出,例如
SIGKILL
(强制终止)和SIGSEGV
(段错误)。 - 可以通过信号处理函数来捕获某些信号并进行适当的处理,但对于一些强制终止的信号,进程无法阻止退出。
- 例如:
- 进程可以接收到来自操作系统或其他进程发送的信号。某些信号会导致进程异常退出,例如
#include <signal.h>
void signalHandler(int signum) {
// 处理信号
printf("Received signal %d\n", signum);
exit(signum); // 根据信号决定退出状态码
}
int main() {
signal(SIGINT, signalHandler); // 捕获中断信号(Ctrl+C)
while (1) {
// 程序的主要逻辑
}
return 0;
}
4,进程的的等待
一、使用wait
和waitpid
函数
wait
函数:wait
函数用于等待任意一个子进程结束。如果调用wait
的进程没有子进程,那么它会立即返回 -1。wait
函数会阻塞当前进程,直到有一个子进程结束。当一个子进程结束时,wait
函数会收集子进程的退出状态,并返回结束的子进程的进程 ID。- 函数原型:
pid_t wait(int *status);
- 参数
status
是一个整数指针,用于接收子进程的退出状态。如果不需要获取退出状态,可以将其设置为NULL
。 - 例如:
#include <stdio.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("Child process.\n");
_exit(0);
} else if (pid > 0) {
// 父进程
int status;
pid_t terminated_pid = wait(&status);
if (terminated_pid == -1) {
perror("wait");
return 1;
}
if (WIFEXITED(status)) {
printf("Child process %d exited with status %d.\n", terminated_pid, WEXITSTATUS(status));
}
} else {
perror("fork");
return 1;
}
return 0;
}
waitpid
函数:waitpid
函数比wait
函数更加灵活,可以等待特定的子进程,并且可以设置一些选项来控制等待的行为。- 函数原型:
pid_t waitpid(pid_t pid, int *status, int options);
- 参数
pid
指定要等待的子进程的进程 ID。如果pid
为 -1,则等待任意一个子进程。 - 参数
options
可以是 0 或者由一些常量组合而成,用于指定等待的选项。例如,WNOHANG
表示非阻塞等待,如果没有子进程结束,立即返回 0。 - 例如:
#include <stdio.h>
#include <sys/wait.h>
int main() {
pid_t pid1 = fork();
if (pid1 == 0) {
// 第一个子进程
printf("First child process.\n");
_exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// 第二个子进程
printf("Second child process.\n");
_exit(2);
}
int status;
pid_t terminated_pid = waitpid(pid2, &status, 0);
if (terminated_pid == -1) {
perror("waitpid");
return 1;
}
if (WIFEXITED(status)) {
printf("Child process %d exited with status %d.\n", terminated_pid, WEXITSTATUS(status));
}
return 0;
}
二、等待的意义
- 资源回收:
- 当子进程结束时,它可能占用一些系统资源,如内存、文件描述符等。通过等待子进程,父进程可以确保这些资源被正确回收,避免资源泄漏。
- 获取子进程的退出状态:
- 父进程可以通过等待获取子进程的退出状态,从而了解子进程的执行结果。这对于错误处理和程序的逻辑控制非常重要。
- 同步:
- 等待子进程可以实现父进程和子进程之间的同步。例如,父进程可以在子进程完成某些任务后再继续执行。
5,进程的替换
一、exec
系列函数介绍
-
exec
函数的作用:exec
系列函数用于在当前进程的地址空间中执行一个新的程序,从而替换当前正在运行的进程。- 新程序的代码、数据和栈将替换原进程的相应部分,而进程的 ID 保持不变。
-
常见的
exec
函数:execl
、execlp
、execle
:这些函数以列表的形式接收命令行参数。execv
、execvp
、execve
:这些函数以数组的形式接收命令行参数。
二、函数原型及参数说明
-
execl
函数原型:int execl(const char *path, const char *arg0,..., (char *)0);
path
:新程序的路径名。arg0
、arg1
等:新程序的命令行参数,最后一个参数必须是NULL
。
-
execv
函数原型:int execv(const char *path, char *const argv[]);
path
:新程序的路径名。argv
:一个以NULL
结尾的字符串数组,包含新程序的命令行参数。
三、示例代码
使用execl
函数的示例:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Before exec.\n");
execl("/bin/ls", "ls", "-l", NULL);
perror("exec failed");
return 0;
}
使用execv
函数的示例:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Before exec.\n");
char *argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
perror("exec failed");
return 0;
}
四、注意事项
- 错误处理:
- 如果
exec
函数调用成功,新程序将替换当前进程,并且不会返回。如果调用失败,会返回 -1,并设置errno
来指示错误原因。可以使用perror
函数来打印错误信息。
- 如果
- 环境变量:
execle
和execve
函数可以接收一个额外的参数来设置新程序的环境变量。
- 进程替换的效果:
- 进程替换后,原进程的代码、数据和栈被新程序的相应部分替换,但进程的 ID、打开的文件描述符等资源通常会保持不变。
进程替换在实际应用中非常有用,例如在 shell 脚本中执行外部命令、启动新的应用程序等场景。
六使用进程的注意事项
一、资源管理
-
内存管理:
- 进程在运行过程中可能会动态分配内存。确保在不再需要时及时释放内存,以避免内存泄漏。可以使用
malloc
、calloc
等函数分配内存,并使用free
函数释放。 - 注意内存访问越界的问题,避免访问未分配或已释放的内存区域,这可能导致程序崩溃或出现不可预测的行为。
- 进程在运行过程中可能会动态分配内存。确保在不再需要时及时释放内存,以避免内存泄漏。可以使用
-
文件描述符:
- 进程可能会打开文件、网络连接等资源,这些资源通常由文件描述符表示。在进程结束前,确保关闭不再需要的文件描述符,以释放系统资源。
- 注意文件描述符的正确使用和管理,避免出现文件描述符泄漏或错误的文件操作。
二、错误处理
-
系统调用错误:
- 当使用系统调用函数(如
fork
、exec
、wait
等)时,要检查返回值以确定是否发生错误。如果发生错误,系统调用通常会返回 -1,并设置errno
变量来指示错误原因。 - 使用
perror
函数可以方便地打印错误信息,帮助调试问题。
- 当使用系统调用函数(如
-
异常情况处理:
- 考虑进程可能遇到的各种异常情况,如被其他进程发送信号中断、资源不足等。可以使用信号处理函数来处理特定的信号,以确保进程在异常情况下能够正确地退出或进行适当的恢复操作。
三、进程间通信
-
同步与互斥:
- 如果多个进程需要共享资源或进行协作,需要考虑同步和互斥问题。可以使用信号量、互斥锁等机制来确保对共享资源的正确访问,避免出现竞争条件和数据不一致的情况。
- 注意同步机制的正确使用和避免死锁的发生。
-
通信方式选择:
- 根据实际需求选择合适的进程间通信方式,如管道、消息队列、共享内存等。不同的通信方式有不同的特点和适用场景,要根据具体情况进行选择。
- 确保通信的可靠性和安全性,避免数据丢失或被篡改。
四、性能考虑
-
进程创建和销毁开销:
- 创建和销毁进程会带来一定的系统开销,包括内存分配、资源初始化等。如果需要频繁地创建和销毁进程,可能会影响系统性能。
- 考虑是否可以使用其他方式(如线程)来减少进程创建的开销,或者优化进程的生命周期管理。
-
进程调度:
- 操作系统的进程调度算法会影响进程的执行效率。了解操作系统的调度策略,合理设置进程的优先级和资源需求,以提高进程的响应时间和系统的整体性能。
五、可移植性
-
不同操作系统的差异:
- C 语言在不同的操作系统上可能有不同的行为和实现。在编写涉及进程的代码时,要考虑到不同操作系统之间的差异,尽量使用可移植的代码和函数。
- 例如,在 Unix/Linux 和 Windows 系统上,进程创建、等待和通信的函数可能不同,需要进行适当的条件编译或使用跨平台的库。
-
编译器差异:
- 不同的编译器可能对 C 语言的标准实现有一些差异。在使用特定的编译器时,要注意其对进程相关功能的支持和行为。
总之,在 C 语言中使用进程需要仔细考虑资源管理、错误处理、进程间通信、性能和可移植性等方面的问题,以确保程序的正确性、可靠性和高效性。