一,进程的基本认识
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 语言中使用进程需要仔细考虑资源管理、错误处理、进程间通信、性能和可移植性等方面的问题,以确保程序的正确性、可靠性和高效性。



















