Linux进程
初识基本概念
1.什么是进程
- 什么是程序、什么是进程,有什么区别
- 程序是一系列指令的集合,这些指令告诉计算机如何执行特定的任务。程序通常是以文件的形式存储在硬盘上的,它们是由程序员编写的代码,经过编译后转换成计算机能够理解的机器语言。程序是静态的,意味着它们在磁盘上是不变的,只有当它们被执行时,才会变成动态的进程。
- 进程是程序的一次执行实例。当程序被启动时,操作系统会创建一个进程,并为其分配必要的资源,如内存和CPU时间。进程是动态的,它们在执行期间可能会改变状态,比如从运行状态变为等待状态。每个进程都有自己的地址空间,这意味着它们可以独立于其他进程运行。
- 2 如何查看系统中有哪些进程
- ps -aux
- top 类似win的任务管理器
- 3 什么是进程标识符(PID)
- 是用来唯一标识系统中的每个进程的非负整数,类似于每个人的身份证号。特殊的PID值有特定含义:
- PID=0:通常被称为交换进程(swapper)或调度进程,主要负责进程调度和系统资源管理。
- PID=1:是系统的初始化进程(init),负责启动和管理系统上的其他所有进程,完成系统初始化任务。
- 4 什么叫父进程,什么叫子进程
- 谁创建 谁是父
- 被创建出来的是子
2.并行&并发
- 并发(Concurrency)指的是多个任务在同一时间段内被执行,但不一定同时。在单核CPU上,通过时间分片的方式,可以实现任务的并发执行,即操作系统轮流给每个任务分配CPU时间,从宏观上看似乎是同时进行的。
- 并行(Parallelism)指的是多个任务确实是在同一时刻同时进行的。这通常需要多个处理单元(例如,多核CPU或GPU中的成百上千个核心)同时执行不同的任务。
3.进程创建
3.1子进程的创建
- 一个现有进程可以调用
fork
函数创建一个新进程,这个新进程被称为子进程(child process)。
3.2fork函数的返回值
- 子进程中:返回0
- 父进程中:返回子进程的ID
- 出错时:返回-1
3.3父子进程的执行顺序
- fork创建子进程后,父子进程的执行顺序没有固定规律,完全取决于操作系统的调度算法。
3.4父子进程的资源复制
- 子进程是父进程的副本,它获得父进程数据空间、堆和栈的副本。
- 需要注意的是,这些是子进程所拥有的副本,父子进程并不共享这些存储空间部分。
3.5正文段的共享
- 当一个进程通过fork()创建子进程时,父子进程共享同一份程序代码(即正文段),以提高内存效率。
3.6为什么需要创建进程
为了实现并发执行和资源的独立分配,fork()函数用于创建一个新的进程,它是父进程的副本。
-
使用fork()创建子进程的一些主要原因:
-
并发性:通过创建子进程,可以同时运行多个程序或任务,提高系统的利用率和效率。
-
隔离性:每个进程都有自己的地址空间,这意味着它们可以独立地运行而不会相互干扰。
-
灵活性:子进程可以修改自己的数据段、堆和栈而不影响父进程或其他子进程,因为这些部分通常在创建时就进行了复制。
-
资源管理:即使父进程结束,其子进程仍然可以继续运行,这允许资源的独立管理和持久性。
-
错误隔离:如果一个进程崩溃,它通常只会影响自己,而不是整个系统或其它进程。
-
3.7 完全拷贝和写时拷贝
完全拷贝(父子进程完全独立)还是写时拷贝(父子进程共享内存)
vfork
- vfork直接使用父进程存储空间、不拷贝
- vfork保证子进程先运行,当子进程调用exit后,父进程才执行(会堵塞)
4.进程退出
4.1正常退出(5个)
- Main函数调用
return
- 进程调用
exit()
,标准c库 - 进程调用
_exit()
或者_Exit()
,属于系统调用 - 补充:
- 进程最后一个线程返回
- 最后一个线程调用
pthread_exit
exit()是对_exit()
和_Exit()
的封装
并且exit() 会冲刷缓冲区,对运行产生的变量做处理
4.2异常退出
- 调用abort
- 当进程收到某些信号时,如ctrl+C
- 最后一个线程对取消(cancellation)请求做出响应
4.3 进程状态
- “S”状态代表可中断的睡眠状态(TASK_INTERRUPTIBLE)
- “R”状态代表进程正在运行或者在运行队列中等待CPU资源。
- “D”状态表示进程处于不可中断的睡眠状态(TASK_UNINTERRUPTIBLE),通常发生在磁盘IO操作期间,此时即使收到信号也不会唤醒进程。
- “T”状态表示进程已被停止,一般是收到了SIGSTOP、SIGSTP、SIGTIN或SIGTOU信号。
- “Z”状态是僵尸状态,表明子进程已经退出,但其父进程尚未收集其资源,导致其进程描述符仍然存在。
- “X”状态代表进程已经结束,其任务在任务列表中不再可见。
5.进程等待
- 为什么要等待子进程退出
- 创建子进程目的,就是找人干活,干完活需要验收(收集子进程状态信息)
- 使用wait waitpid收集子进程退出状态
- wait waitpid 后子进程不会变为僵尸进程
- 父进程等待子进程退出并收集子进程的退出状态
- 子进程退出状态不被收集,变成僵死进程(僵尸进程)
注意:
关于“等待”的含义:等待指的是父进程通过特定的系统调用(如wait或waitpid)主动进入阻塞状态,直到子进程退出。这个过程中,父进程暂停执行,直到收到子进程退出的通知。
wait函数详解
wait 函数在Unix-like操作系统中用于挂起调用进程,直到其中一个子进程结束。
wait 函数的原型如下:
#include <sys/types.h>
#include <sys/wait.h>
pidt wait(int status);
参数:
- status:这是一个指向 int 类型的指针,用于存储子进程的退出状态。如果 status 不是 NULL,那么在子进程结束后,wait 函数会将子进程的退出状态存储在这个指针指向的内存位置。如果 status 是 NULL,则 wait 函数不会返回子进程的退出状态。
返回值
- wait 函数的返回值是一个 pidt 类型的值,表示子进程的进程ID(PID)。返回值的含义如下:
- 如果 wait 成功返回了一个子进程的PID,则表示有一个子进程已经结束。
- 如果 wait 返回 -1,则表示出错,可以通过 errno 变量来获取错误信息。
waitpid 函数详解
原型
pidt waitpid(pidt pid, int status, int options);
参数
pid
- 类型:pidt
- 描述:指定要等待的子进程的进程ID(PID)。
- 取值:
-1
:等待任意一个子进程结束。>1
:等待具有指定PID的子进程结束。<0
:等待进程组ID为 abs(pid) 的任意子进程结束。
status
- 类型:int
- 描述:一个指向 int 类型的指针,用于存储子进程的退出状态。
- 取值:
- 非 NULL:waitpid 会将子进程的退出状态存储在这个指针指向的内存位置。
- NULL:不返回子进程的退出状态。
options
- 类型:int
- 描述:一组标志,用于控制 waitpid 的行为。
- 取值:
- 0:默认行为,阻塞等待直到子进程结束。
- WNOHANG:非阻塞模式,如果没有任何子进程结束,则立即返回 -1,errno 设置为 ECHILD。
- WUNTRACED:如果设置了这个标志,waitpid 也会返回处于停止状态的子进程的信息,即使它们没有退出。
- WCONTINUED(某些系统特有):如果设置了这个标志,waitpid 也会返回继续执行的子进程的信息。
返回值
-
类型:pidt
-
描述:返回值表示子进程的PID。
-
取值:
>0
:成功返回了一个子进程的PID,表示该子进程已经结束。- 0:如果设置了 WNOHANG 标志,并且没有子进程结束,则返回 0。
- -1:出错,可以通过 errno 变量来获取错误信息。
错误码
-
类型:errno
-
描述:当 waitpid 返回 -1 时,errno 变量会被设置为相应的错误码。
-
取值:
- ECHILD:调用进程没有子进程,或者设置了 WNOHANG 标志且没有子进程结束。
- EINVAL:pid 参数无效(例如,尝试等待一个不存在的进程)。
waitpid 函数提供了比 wait 函数更多的灵活性,允许父进程指定等待特定的子进程,或者以非阻塞的方式等待子进程结束。通过设置不同的选项,父进程可以根据需要选择合适的等待策略。
6.孤儿进程
父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程
Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程
7.exec族函数
exec
系列函数用于在当前进程中执行另一个程序,而不会创建新进程。
根据不同的参数格式和执行环境,可以选择使用 execl
、execlp
、execv
或 execvp
。
1. execl
-
原型:
int execl(const char *path, const char *arg0, ..., NULL);
-
特点:
- 需要明确指定可执行文件的路径(不搜索
PATH
环境变量)。 - 参数是一个以
NULL
结尾的可变参数列表(类似argv
数组的展开)。
- 需要明确指定可执行文件的路径(不搜索
-
使用技巧:
- 明确路径:适用于你知道程序的绝对路径或相对路径的情况。
- 固定参数数量:适合程序参数较少、且你不需要动态构造参数列表的场景。
- 以 NULL 结束:必须确保最后一个参数是
NULL
,否则会出现未定义行为。
-
示例:
execl("/bin/ls", "ls", "-l", NULL);
2. execlp
-
原型:
int execlp(const char *file, const char *arg0, ..., NULL);
-
特点:
- 类似于
execl
,但会自动搜索PATH
环境变量来查找可执行文件。 - 参数也是一个以
NULL
结尾的可变参数列表。
- 类似于
-
使用技巧:
- 搜索
PATH
:适用于不想提供程序的绝对路径,而希望系统在PATH
环境变量中找到可执行文件的场景。 - 无需明确路径:可以简化代码,只提供可执行文件的名称。
- 搜索
-
示例:
execlp("ls", "ls", "-l", NULL); // 系统自动从 PATH 中找到 ls
3. execv
-
原型:
int execv(const char *path, char *const argv[]);
-
特点:
- 需要明确指定可执行文件的路径(不搜索
PATH
环境变量)。 - 参数是一个
argv
数组(类似main
函数的argv
参数)。
- 需要明确指定可执行文件的路径(不搜索
-
使用技巧:
- 动态参数:适用于参数列表动态生成或变量控制参数的场景,因为你可以通过构造一个
argv
数组来传递参数。 - 数组灵活性:适合参数数量不固定的场景,可以根据程序逻辑动态构建
argv
数组。
- 动态参数:适用于参数列表动态生成或变量控制参数的场景,因为你可以通过构造一个
-
示例:
char *args[] = { "ls", "-l", NULL }; execv("/bin/ls", args);
4. execvp
-
原型:
int execvp(const char *file, char *const argv[]);
-
特点:
- 类似于
execv
,但会自动搜索PATH
环境变量来查找可执行文件。 - 参数也是一个
argv
数组。
- 类似于
-
使用技巧:
- 动态参数和
PATH
搜索:适合动态构建参数列表并且希望系统自动从PATH
中查找可执行文件的场景。 - 组合功能:既具备数组的灵活性,又简化了路径查找的代码。
- 动态参数和
-
示例:
char *args[] = { "ls", "-l", NULL }; execvp("ls", args); // 自动从 PATH 中找到 ls
总结
execl
和execlp
适合固定参数且命令简单的情况。execv
和execvp
提供了更多的灵活性,适合动态参数列表的场景。- 当需要搜索
PATH
环境变量时,选择带有p
后缀的函数(execlp
、execvp
)。
8.system函数的使用技巧
system()
函数是标准库中的一个函数,属于 <stdlib.h>
头文件。它用于在 C 程序中调用操作系统的命令解释器执行某些系统命令。
函数原型
int system(const char *command);
参数说明
command
: 一个指向以空字符结尾的字符串指针,该字符串包含要执行的命令。如果command
是NULL
,则system()
只检查命令解释器是否存在。
返回值
- 成功:返回命令的退出状态(由系统定义)。
- 失败:返回非零值。具体的值取决于系统。
使用示例
- 执行简单命令:
#include <stdlib.h>
#include <stdio.h>
int main() {
int result = system("ls -l"); // 执行系统命令 ls -l
if (result == -1) {
perror("执行失败");
}
return 0;
}
该程序将调用系统的 ls -l
命令,列出当前目录下的文件和详细信息。
- 检查命令解释器是否存在:
#include <stdlib.h>
#include <stdio.h>
int main() {
int result = system(NULL); // 检查命令解释器是否存在
if (result == 0) {
printf("命令解析器不可用\n");
} else {
printf("命令解析器可用\n");
}
return 0;
}
如果 system(NULL)
返回 0,表示命令处理器不可用;否则表示命令处理器可用。
注意事项
- 安全性:
system()
函数是一个潜在的安全风险,尤其是在处理来自不可信来源的输入时,因为它会在命令解释器中执行字符串中的命令。 - 效率:使用
system()
会创建一个新的进程,并调用命令解释器,这可能会比较慢。如果只是为了执行简单的任务(比如列出文件),可以考虑直接使用fork()
和exec()
系列函数来更高效地完成工作。
总结
system()
是一个简单的方式来执行系统命令,适合快速调用一些外部命令。- 对于更复杂的进程管理,建议使用
fork()
和exec()
函数。
9.popen函数使用技巧
popen()
函数是标准库中的一个函数,它用于创建一个管道,并打开一个进程以便执行系统命令。通过这个管道,程序可以读取或写入该进程的标准输入或标准输出。
函数原型
FILE *popen(const char *command, const char *type);
参数说明
command
: 一个指向以空字符结尾的字符串,表示要执行的系统命令。type
: 一个字符串,可以是"r"
或"w"
,表示管道的模式:"r"
:表示程序将从命令的输出中读取数据。"w"
:表示程序将向命令的输入中写入数据。
返回值
- 成功:返回指向
FILE
的指针,它是一个标准 I/O 流,可以用于读写操作。 - 失败:返回
NULL
,并设置errno
来指示错误。
使用示例
- 从命令读取数据
#include <stdio.h>
#include <stdlib.h>
int main() {
char buffer[128];
FILE *fp;
// 执行系统命令并读取输出
fp = popen("ls -l", "r");
if (fp == NULL) {
perror("popen failed");
exit(1);
}
// 逐行读取命令输出
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
// 关闭管道
pclose(fp);
return 0;
}
在这个例子中,popen()
执行 ls -l
命令,并通过管道读取其输出。
- 向命令写入数据
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp;
// 向 `sort` 命令的输入中写入数据
fp = popen("sort", "w");
if (fp == NULL) {
perror("popen failed");
exit(1);
}
// 向 sort 命令写入数据
fprintf(fp, "banana\n");
fprintf(fp, "apple\n");
fprintf(fp, "cherry\n");
// 关闭管道
pclose(fp);
return 0;
}
在这个例子中,popen()
调用了 sort
命令,并通过管道向它的标准输入中写入数据。
pclose()
popen()
创建的管道必须使用 pclose()
关闭。
int pclose(FILE *stream);
- 参数:要关闭的管道流。
- 返回值:返回命令的退出状态。如果失败则返回
-1
。
注意事项
- 阻塞行为:
popen()
是阻塞的,调用时会等待命令完成执行。 - 安全性:和
system()
类似,popen()
也可能存在安全隐患,尤其是在使用来自不可信输入的数据构造命令时。 - 使用场景:
popen()
常用于需要与命令进行数据交互的场景,而不仅仅是执行命令。
三者的区别
10.函数
getpid和getppid函数
函数功能
getpid 获得当前进程的id号
getppid 获得当前进程父进程的id号
函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
参数说明
无
返回值
- 对应的进程id号
fork函数
函数功能
创建一个新进程
函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
参数说明
无
返回值
- 在子进程中,fork()函数返回0。
- 在父进程中,fork()返回新创建的子进程的进程ID(PID)。
- 如果发生错误,fork()返回-1。
vfork函数
函数功能
用于创建新进程的
函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
参数说明
无
返回值
- 在子进程中,vfork()函数返回0。
- 在父进程中,vfork()返回新创建的子进程的进程ID(PID)。
- 如果发生错误,vfork()返回-1。
特点
它与fork()函数类似,但有一些重要的区别。vfork()函数在父进程和子进程之间共享相同的地址空间,这意味着子进程可以直接访问父进程的变量、函数和堆栈等。