进程指的是程序在操作系统内的一次执行过程。它不只是程序代码,还涵盖了程序运行时的各类资源与状态信息。包括创建、调度、消亡。
就绪状态:进程已经准备好运行,正在等待操作系统分配CPU资源。
运行状态:进程正在CPU上执行。
阻塞状态:进程因为等待某个事件(如I/O操作完成)而暂时无法继续执行。
可唤醒等待态S:进程挂起等在某个资源到达后继续向下执行。
不可唤醒等待态D:进程任务挂起直到等到某个信号来了才继续向下执行。
暂停态T:进程任务挂起,直到CPU发送指令才能继续向下执行。
僵尸态Z:代码已经执行完毕,空间仍然存在。
结束态X:代码执行完毕,代码被回收。
// 程序的主函数,程序的执行从这里开始
int main()
{
// 定义一个 pid_t 类型的变量 pid,用于存储 fork 函数的返回值
pid_t pid;
// 调用 fork 函数创建一个新的子进程
// fork 函数会返回两次,在父进程中返回子进程的进程 ID,在子进程中返回 0,出错时返回 -1
pid = fork();
// 检查 fork 函数的返回值是否小于 0,如果小于 0 表示创建子进程时出错
if (pid < 0)
{
// 使用 fprintf 函数向标准错误输出流输出错误信息,提示 fork 操作失败
fprintf(stderr, "Fork failed\n");
// 返回 1 表示程序异常退出
return 1;
}
// 检查 fork 函数的返回值是否等于 0,如果等于 0 表示当前代码在子进程中执行
else if (pid == 0)
{
// 输出提示信息,表明当前是子进程,并使用 getpid 函数获取当前子进程的进程 ID 并打印
printf("This is the child process, pid = %d\n", getpid());
}
// 如果 fork 函数的返回值大于 0,说明当前代码在父进程中执行,返回值即为子进程的进程 ID
else
{
// 输出提示信息,表明当前是父进程,并打印子进程的进程 ID
printf("This is the parent process, child pid = %d\n", pid);
}
// 程序正常结束,返回 0
return 0;
}
用task_struct结构体来表示一个进程。这个结构体在linux内核里定义,包含了进程的所有信息,像进程状态、进程ID、优先级、打开的文件等。
task_struct源于linux内核源码中的include/linux/sched.h文件。
里面的volatile long state ,原来表示进程的当前状态。
pid_t pid :进程的 唯一标识符。
struct task_struct *parent :指向父进程的指针。
struct list_head children :子进程链表。
C语言系统本身并不会区分a.out这个特定的执行文件,而是去区分运行着的进程。
每个进程都有独一无二的进程ID(PID)。当启动一个a.out 程序时,操作系统会给它分配一个PID,PID是一个正整数,在系统里是唯一的,操作系统能够借助PID来区分不同的进程。
可以通过getpid()函数获取当前进程的PID。
操作系统为每个进程都维护了一个进程控制块(PCB),它是一个数据结构,里面包含了进程的各种信息,像PID、进程状态、程序计数器、内存指针、文件描述符等。操作系统会依据PCB来管理和区分不同的进程。
每个进程都有一个父进程,父进程ID(PPID)同样能用来区分进程。可以用getppid()函数获取当前进程的父进程ID。
即使是同一个可执行文件a.out ,要是在启动时使用了不同的命令行参数或者环境变量,操作系统也能把他们区分开来。
32bit 的系统中,给用户层分配的内存空间通常为4GB,
存储内容:存放程序的可执行指令,也就是编译后的机器码。
特性:只读,为了防止程序在运行过程中意外修改自身的指令。
共享,当多个进程运行同一个程序时,这些进程可以共享这一个代码段。
存储内容:存储已初始化的全局变量和静态变量,这些变量的值非0。
例如:int a = 10;
存储内容:未初始化的全局变量和静态变量会被存放在BSS段中,在程序开始执行前,系统会自动把这些变量初始化为0。
例如:int a ;
存储内容:动态分配的内存就存放在堆中,如使用malloc、calloc、realloc。
特性:堆的内存分配是由低地址向高地址增长的,且使用完后需要通过free()来释放内存,否则会造成内存泄漏。
存储内容:函数调用的上下文,也就是栈帧,存放在栈中。栈帧里包含局部变量、函数参数以及返回地址等信息。
特性:栈的内存分配是从高地址向低地址增长的,它的空间大小是有限的,一旦超出限制就会导致栈溢出。
存储内容:程序运行时的命令行参数以及环境变量存放在这个区域。
位置:位于用户空间的最高地址处。
高地址
┌─────────────────────────────────────┐
│ │
│ 环境变量和命令行参数 │
│ │
├─────────────────────────────────────┤
│ │
│ 栈 │
│ (向低地址方向增长) │
│ │
├─────────────────────────────────────┤
│ │
│ 堆 │
│ (向高地址方向增长) │
│ │
├─────────────────────────────────────┤
│ │
│ BSS段 │
│ (未初始化的全局变量) │
│ │
├─────────────────────────────────────┤
│ │
│ 数据段 │
│ (已初始化的全局变量) │
│ │
├─────────────────────────────────────┤
│ │
│ 代码段 │
│ (程序指令) │
│ │
└─────────────────────────────────────┘
低地址
虚拟地址是运行程序时 CPU 所生成的地址。
内存隔离:不同进程的虚拟地址空间相互独立,这样一个进程出现问题,不会对其他进程造成影响。
内存抽象化:程序所使用的内存空间是连续的,然而物理内存可能是分散的。
内存保护:通过设置访问权限,能够防止程序对其他进程的内存进行越界访问。
内存共享:不同进程可以共享同一块物理内存,例如共享库。
指针实际上就是虚拟地址:
int a = 10;
int *p = &a; // p存储的是变量a的虚拟地址
系统会给每个进程分配独立的系统资源,像内存空间、文件描述符、CPU 时间片等。
int main()
{
// 每个进程都有自己独立的内存空间
int pid = fork(); // 创建子进程
if (pid == 0)
{
// 子进程
printf("子进程: PID=%d\n", getpid());
}
else
{
// 父进程
printf("父进程: PID=%d\n", getpid());
}
return 0;
}
借助进程,系统可以同时运行多个程序,也就是实现并发或并行操作。
int main()
{
pid_t pid = fork(); // 创建子进程
if (pid < 0)
{
perror("创建进程失败");
exit(EXIT_FAILURE);
}
if (pid == 0)
{
// 子进程执行新程序
execlp("/bin/ls", "ls", "-l", NULL);
}
else
{
// 父进程继续执行其他任务
printf("父进程继续运行...\n");
wait(NULL); // 等待子进程结束
}
return 0;
}
各个进程之间相互隔离,一个进程出现崩溃或者异常,通常不会对其他进程造成影响。
int main()
{
pid_t pid = fork();
if (pid == 0)
{
// 子进程故意出错
printf("子进程准备崩溃...\n");
*((int*)0) = 10; // 访问无效地址,引发段错误
}
else
{
// 父进程不受影响
printf("父进程继续正常运行\n");
sleep(2);
}
return 0;
}
在处理 I/O 密集型或者计算密集型任务时,通过创建多个进程,可以显著提高程序的执行效率
#define N 5
int main()
{
int i = 0;
for (i = 0; i < N; i++)
{
pid_t pid = fork();
if (pid == 0)
{
printf("子进程 %d 执行计算任务\n", i);
exit(EXIT_SUCCESS);
}
}
// 父进程等待所有子进程结束
for (i = 0; i < N; i++)
{
wait(NULL);
}
printf("所有子进程执行完毕\n");
return 0;
}
不同进程之间可以通过管道、消息队列、共享内存等机制进行通信,从而实现更复杂的分布式系统。
int main()
{
int pipefd[2]; // 创建管道文件描述符数组:pipefd[0]为读端,pipefd[1]为写端
char buffer[100]; // 用于存储从管道读取的数据的缓冲区
// 创建匿名管道,失败时返回-1并设置errno
if (pipe(pipefd) == -1)
{
perror("创建管道失败"); // 输出系统错误信息
return 1; // 非零返回值表示程序异常退出
}
pid_t pid = fork(); // 创建子进程,返回值:子进程返回0,父进程返回子进程PID
if (pid == 0)
{
// 子进程执行区域
close(pipefd[0]); // 关闭不需要的读端,避免资源泄漏
const char* message = "Hello from child process!";
// 向管道写端写入字符串(包含字符串结束符'\0')
write(pipefd[1], message, strlen(message) + 1);
close(pipefd[1]); // 写入完成后关闭写端,通知读端数据已写完
}
else
{
// 父进程执行区域
close(pipefd[1]); // 关闭不需要的写端
// 从管道读端读取数据到缓冲区,最多读取sizeof(buffer)-1字节
read(pipefd[0], buffer, sizeof(buffer));
printf("父进程收到消息: %s\n", buffer); // 输出从子进程接收的消息
close(pipefd[0]); // 读取完成后关闭读端
}
return 0; // 程序正常退出
}
进程正在 CPU 上执行指令。在单 CPU 系统中,同一时刻只有一个进程处于运行态
进程已获得除了 CPU 外的所有必要资源,等待操作系统调度执行,一旦 CPU 空闲,就绪队列中的进程将被选中进入运行态。
进程因等待某个事件(如 I/O 完成、信号量、锁等)而暂停执行。此时进程不占用 CPU 资源,直到等待的事件发生后,进程才会转为就绪态。
进程刚刚被创建,但尚未被操作系统完全初始化。此时进程正在分配资源并进入系统。
进程已执行完毕或被强制终止,但尚未完全释放资源。在终止态下,进程仍保留一些信息(如退出状态码)供父进程查询,之后才会被系统彻底销毁。
进程暂时被移除内存,存储到磁盘上。挂起态通常分为 就绪挂起(进程在磁盘上就绪,等待被调回内存)和阻塞挂起(进程在磁盘上等待事件)。这种状态常见于内存紧张的系统中。
ps命令:查看进程的当前状态(R 表示运行,S 表示睡眠,D 表示不可中断睡眠,T 表示停止,Z 表示僵尸进程)。
/proc/[pid]/status 文件:包含进程的详细状态信息。
系统调用:如wait()、waitpid() 获取子进程的终止状态。
这个系统调用的作用是创建一个子进程,新创建的子进程几乎是父进程的副本。
子进程会获取父进程的数据空间、堆和栈的拷贝。
返回值:在父进程中返回子进程的 PID(进程ID),在子进程中返回0,如果返回-1,就表明创建进程失败。
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork failed");
return 1;
}
else if (pid == 0)
{
printf("子进程: PID = %d\n", getpid());
}
else
{
printf("父进程: 子进程PID = %d, 父进程PID = %d\n", pid, getpid());
}
return 0;
}
vfork()也是创建子进程,但与fork()有区别,vfork()创建的子进程会和父进程共享地址空间,而且在子进程调用exec()或者exit()之前,父进程会被阻塞。
这一系列函数的作用是用新的程序替换当前进程的映像。即当调用exec()之后,当前进程的代码段、数据段和堆栈都会被新程序替换。
常见的有:execl(),execv(),execle(),
主要用于父进程等待子进程结束,并且可以获取子进程的终止状态。
wait(&status):父进程会阻塞,直到任意一个子进程终止。
waitpid(pid,&status,options):父进程等待特定 PID 的子进程,或者通过选项设置为非阻塞模式。
getpid():返回当前进程的 PID。
getppid():返回当前进程父进程的 PID。
用于终止当前进程。
exit(status):会刷新一下缓冲区,然后终止进程。
_exit(status):直接终止进程。
作用是执行一个 shell 命令。它会创建一个子进程来执行指定的命令,父进程会等待子进程结束。
int main()
{
system("ls -l"); // 执行ls -l命令
return 0;
}
用于向进程发送信号,如终止进程、暂停进程等。
用于处理信号。
用于创建一个新的会话。
用fork()函数即可创建新进程。
#include
pid_t fork(void);
负值:表面进程创建失败,可能是系统资源不足,或者进程数量达到了上限。
零:这是子进程的返回结果,子进程可以通过getppid()来获取父进程的 ID。
正整数:这是父进程的返回结果,返回的数值是新创建的子进程的 ID。
子进程会复制父进程的代码段、数据段、堆和栈等内存空间。
子进程会继承父进程的文件描述符、信号处理方式以及当前工作目录。
进程 ID(PID)不一样,子进程的 PID 是唯一的。
父进程 ID(PPID)不同,子进程的 PPID 是创建它的父进程的 PID。
子进程的计时器会重新开始计时。
调用fork()之后,父子进程会并发执行,但执行顺序由操作系统调度器决定。
子进程从fork()返回处开始执行。
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork failed");
return 1;
}
else if (pid == 0)
{
// 子进程执行的代码
printf("子进程: PID = %d, PPID = %d\n", getpid(), getppid());
}
else
{
// 父进程执行的代码
printf("父进程: PID = %d, 子进程PID = %d\n", getpid(), pid());
}
return 0;
}
主要要避免僵尸进程的出现。
当子进程终止时,它的进程描述符不会立刻被释放,而是会保留下来,直至父进程读取到子进程的退出状态信息。要是父进程没有调用wait()或waitpid()来获取子进程的状态,子进程就会变成僵尸进程。
尽管僵尸进程不会占用太多系统资源,但如果这类进程数量过多,就可能导致系统的进程表被占满,使得新进程无法创建。
子进程终止后,它所占用的大部分资源,像内存、文件描述符等,都会被系统回收。
但进程描述符依然会保留,直到父进程处理完相关状态。
父进程可以通过调用wait()或waitpid()来获取子进程的退出状态,从而让子进程的资源得到释放。
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("Fork failed");
return 1;
}
else if (pid == 0)
{
// 子进程
printf("Child process (PID=%d) exiting...\n", getpid());
exit(EXIT_SUCCESS);
}
else
{
// 父进程
printf("Parent process (PID=%d) waiting...\n", getpid());
int status;
wait(&status); // 等待子进程结束
printf("Child process exited with status %d\n", WEXITSTATUS(status));
}
return 0;
}
父进程可以通过设置 SIGCHLD 信号的处理方式为SIG_IGN,让内核自动清理子进程。这样就无需父进程调用wait()了。
int main()
{
// 设置忽略 SIGCHLD 信号,让内核自动清理子进程
signal(SIGCHLD, SIG_IGN);
pid_t pid = fork();
if (pid < 0)
{
perror("Fork failed");
return 1;
}
else if (pid == 0)
{
// 子进程
printf("Child process (PID=%d) exiting...\n", getpid());
exit(EXIT_SUCCESS);
}
else
{
// 父进程可以继续执行其他任务,无需等待子进程
printf("Parent process (PID=%d) continuing...\n", getpid());
sleep(2);
}
return 0;
}
父进程可以使用waitpid()并传入 WNOHANG 参数,以非阻塞的方式查询子进程是否已经终止。
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("Fork failed");
return 1;
}
else if (pid == 0)
{
// 子进程
printf("Child process (PID=%d) sleeping...\n", getpid());
sleep(2);
printf("Child process exiting...\n", getpid());
exit(EXIT_SUCCESS);
}
else
{
// 父进程
printf("Parent process (PID=%d) working...\n", getpid());
int status;
pid_t result;
// 循环检查子进程状态,不阻塞父进程
while ((result = waitpid(pid, &status, WNOHANG)) == 0)
{
printf("Parent: Child is still running...\n");
sleep(1);
}
if (result == pid)
{
printf("Parent: Child exited with status %d\n", WEXITSTATUS(status));
}
}
return 0;
}
需要用到exec系列函数。
1.运用fork()函数创建出子进程。
2.在子进程中调用exec系列函数,像execl 、execv、execle、execve、execlp、execvp
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork失败"); // 输出错误信息
exit(EXIT_FAILURE); // 终止程序
}
else if (pid == 0)
{
// 子进程执行区域
printf("子进程开始执行外部程序...\n");
// 调用execl执行/bin/ls命令
// 参数1:可执行文件路径
// 参数2:命令名(argv[0])
// 参数3及后续:命令行参数(以NULL结尾)
execl("/bin/ls", "ls", "-l", NULL);
// 若execl返回,说明调用失败
perror("execl失败"); // 输出错误原因
exit(EXIT_FAILURE); // 子进程异常退出
}
else
{
// 父进程执行区域
printf("父进程等待子进程结束...\n");
// 等待子进程终止
// wait(NULL)会阻塞父进程直到任意子进程结束
wait(NULL);
printf("子进程已结束\n");
}
return 0;
}
子进程原本的程序代码会被外部程序的代码所取代,而且程序的全局变量也会被重置。
子进程会拥有新的程序计数器、堆栈指针以及寄存器值。
子进程在调用exec之前打开的文件描述符,在exec调用之后依然会保持打开状态,除非这些文件描述符设置了 FD_CLOEXEC 标志
子进程的 PID 不会改变,只是运行的程序发生了变化
子进程原来的内存内容会被丢弃,转而加载新程序的内存映像。
线程属于程序能够独立运行的最小执行单元。存在于进程内部,和同一进程的其他线程共享全局变量、文件描述符等资源,不过各自拥有独立的栈空间与程序计数器。
轻量级:线程的创建和切换开销比进程小,因为他们共享进程的资源。
资源共享:同一进程内的线程共享内存空间、文件句柄等资源,便于数据交换和通信。
并发执行:多个线程可以并发执行,提高程序和执行效率。
独立执行流程:每个线程有自己的执行路径和状态。
通常通过 POSIX 线程库来实现多线程。
// 线程函数,线程启动后会执行这个函数
void* thread_function(void* arg)
{
int thread_id = *(int*)arg;
printf("线程 %d 正在运行\n", thread_id);
// 线程执行一些工作...
pthread_exit(NULL); // 线程退出
}
int main()
{
pthread_t thread1, thread2;
int id1 = 1, id2 = 2;
// 创建两个线程
if (pthread_create(&thread1, NULL, thread_function, &id1) != 0)
{
perror("线程创建失败");
return 1;
}
if (pthread_create(&thread2, NULL, thread_function, &id2) != 0)
{
perror("线程创建失败");
return 1;
}
// 等待线程结束
if (pthread_join(thread1, NULL) != 0)
{
perror("等待线程失败");
return 1;
}
if (pthread_join(thread2, NULL) != 0)
{
perror("等待线程失败");
return 1;
}
printf("所有线程已完成\n");
return 0;
}
编译时需要链接 pthread 库 :-lpthread
由于多线程共享资源,可能会引发竟态条件。需要使用同步机制,如互斥锁、信号量等。
int shared_counter = 0;
pthread_mutex_t mutex;
void* increment(void* arg)
{
int i = 0;
for (i = 0; i < 100000; i++)
{
pthread_mutex_lock(&mutex); // 加锁
shared_counter++;
pthread_mutex_unlock(&mutex); // 解锁
}
pthread_exit(NULL);
}
int main()
{
pthread_t t1, t2;
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex); // 销毁互斥锁
printf("共享计数器的值: %d\n", shared_counter);
return 0;
}
进程:是程序在操作系统中的一次执行实例,是系统进行资源分配和调度的基本单位。
线程:是进程中的一个执行单元,是 CPU 调度和分派的基本单位。
进程:不同进程之间的内存空间是相互独立的,一个进程不能直接访问另一个进程的内存。
线程:同一进程内的线程共享进程的内存空间,他们可以直接访问进程中的全局变量和堆内存。
进程:创建进程时,操作系统需要为其分配独立的内存空间和系统资源,开销比较大。
线程:创建线程时,由于共享进程的资源,只需要为线程分配栈空间和程序计数器等少量资源,开销比较小。
进程:进程间通信(IPC)需要使用特定的机制,如管道、消息队列、共享内存、套接字等。
线程:线程间通信可以直接通过共享全局变量来实现。这种方式简单高效。但要注意线程同步的问题,避免竟态条件。
进程:进程的调度和切换需要保存和恢复进程的上下文,开销比较大。
线程:线程的调度和切换只需要保存和恢复线程的上下文,开销比较小。
进程:一个进程崩溃不会影响其他进程,因为他们的内存空间是独立的。
线程:一个线程崩溃可能会导致整个进程崩溃,因为他们是共享同一内存空间。
// 线程函数
void* thread_function(void* arg)
{
printf("这是一个线程,线程ID: %lu\n", pthread_self());
return NULL;
}
int main()
{
// 创建进程
pid_t pid = fork();
if (pid < 0)
{
// 出错处理
perror("fork失败");
exit(EXIT_FAILURE);
}
else if (pid == 0)
{
// 子进程
printf("这是子进程,进程ID: %d\n", getpid());
}
else
{
// 父进程
printf("这是父进程,进程ID: %d,子进程ID: %d\n", getpid(), pid);
// 创建线程
pthread_t thread_id;
int result = pthread_create(&thread_id, NULL, thread_function, NULL);
if (result != 0)
{
// 出错处理
perror("线程创建失败");
exit(EXIT_FAILURE);
}
// 等待线程结束
pthread_join(thread_id, NULL);
printf("线程已结束\n");
}
return 0;
}