Linux进程
进程基本概念
- 进程是运行中的程序.由程序代码,数据,变量,打开的文件(文件描述符)和一个环境组成
- 多个进程会分配多个这样的场所
- 当一个处理器要管理多个进程时,就要有相应的调度算法,合理分配系统资源
- 为了合理管理进程,操作系统会在内核空间某个位置存放进程的属性信息,这就是PCB
进程id
- 每个进程都有唯一的进程ID号,通过进程ID可以查找到相应的进程及了解进程的当前状态
接口代码
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); // 返回进程ID
pid_t getppid(void); // 返回父进程ID
uid_t getuid(void); // 返回实用户ID
uid_t geteuid(void); // 返回有效用户ID
gid_t getgid(void); // 返回真实组ID
gid_t getegid(void); // 返回有效组ID
进程编程
fork系统调用
- 当前进程通过fork(),可以创建一个子进程,子进程共享父进程的代码段,属性
- 子进程拥有自己独立的数据空间,自己的环境,自己独立的PCB
- 父子进程的执行顺序是系统调度器支配,并不确定优先执行顺序
- 父进程何时创建的子进程,子进程就从相应的代码处开始继续执行
接口代码
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回值
当子进程创建成功返回 0 ,创建失败返回 -1
实例代码
简单示例
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main()
{
pid_t t;
t = fork();
printf("当前进程ID%d\n",getpid());
return 0;
}
复杂示例
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main()
{
pid_t pid;
int n;// 循环打印信息次数
char* message;// 打印提示信息
pid = fork();
switch(pid)
{
case(-1):
message = "子进程创建失败";
printf("%s\n",message);
case(0):
message = "当前在子进程";
n = 3;
break;
default:
message = "当前在父进程";
n = 5;
break;
}
while(n --)
printf("%s\n",message);
return 0;
}
// 输出结果
/**
当前在父进程
当前在子进程
当前在父进程
当前在子进程
当前在父进程
当前在子进程
当前在父进程
当前在父进程
*/
vfork系统调用
与fork类似,但有两点不同
vfork使用父子进程共享虚拟内存空间
这种共享内存的方式意味着子进程的操作实际上是在父进程的内存段上进行的,因此子进程对数据段、堆或栈的任何改变都将在父进程恢复执行时可见。这种机制允许子进程直接使用父进程的存储空间,而不需要复制虚拟内存页或页表,从而节省了资源并提高了效率
vfork保证子进程先运行
直到子进程调用exec()或exit()之后,父进程才会被调度运行
接口代码
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
返回值
当子进程创建成功返回 0 ,创建失败返回 -1
实例代码
验证子进程先运行
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
pid = vfork();
if(pid < 0)printf("进程创建失败\n");
else if(pid == 0)
{
sleep(1);
printf("当前是子进程\n");
}
else printf("当前是父进程\n");
exit(0);
}
// 输出结果
/**
当前在子进程
当前在父进程
*/
验证共享虚拟内存空间
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid < 0)printf("子进程创建失败\n");
else if(pid == 0)
{
for(;cnt!=3;cnt++)
printf("当前是子进程第%d次\n",cnt);
}
else printf("当前是父进程第%d次\n",cnt);
exit(0);
}
// 输出结果
/**
当前是子进程第0次
当前是子进程第1次
当前是子进程第2次
当前是父进程第3次
*/
wait系统调用
- wait系统调用,主要起一个给子进程收尸的作用,当子进程运行完毕后,会释放大部分资源,遗留一小部分资源未释放,这时该进程就成为僵尸进程,需要wait进行收尸
- 在父进程中使用wait时,会等待子进程运行结束,父进程再开始接着运行
接口代码
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* stat_loc);
返回值
- WIFEXITED(stat_val) 如果子进程正常结束,取非零值
- WEXITSTATUS(stat_val) 如果WIFEXITED非零,返回子进程的退出码(子进程id)
- WIFSIGNALED(stat_val) 如果子进程因为一个未捕获的信号终止,取非零值
- WTERMSIG(stat_val) 如果WIFSIGNALED非零,返回一个信号代码
- WIFSTOPPED(stat_val) 如果子进程意外终止,取一个非零值
- WSTOPSIG(stat_val) 如果WIFSTOPPED非零,返回一个信号代码
参数解释
- stat_loc: 状态信息写入的位置,可以为NULL
实例代码
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid ;
pid = fork();
if(pid == 0)
{
printf("子进程创建成功,子进程PID = %d\n",getpid());
}
else
{
printf("父进程准备进入等待\n");
pid_t childPid = wait(NULL);
printf("父进程退出等待状态,子进程PID = %d\n",childPid);
}
exit(0);
}
// 输出结果
/**
父进程准备进入等待
子进程创建成功,子进程PID = 3903
父进程退出等待状态,子进程PID = 3903
*/
exec系统调用
对于上面的fork和vfork函数,都是父子进程之间调度,显然很不符合开发业务实际情况,so看exec
exec: 可以把当前进程替换为一个新的进程,并转换到调用进程的内存空间
返回值处理
如果
exec
系列函数失败,它们会返回 -1,并设置errno
来说明具体的错误原因。常见的错误包括:
- ENOENT:找不到指定的文件。
- EACCES:没有执行权限。
- EINVAL:传递的参数无效。
- ENOMEM:内存不足,无法加载新程序。
你可以通过
perror()
或strerror(errno)
来查看错误信息
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
char *args[] = {"nonexistent_program", NULL}; // 试图执行一个不存在的程序
printf("Before execvp\n");
// 尝试执行一个不存在的程序
if (execvp("nonexistent_program", args) == -1) {
// 如果 execvp 失败,返回 -1,并设置 errno
printf("Error: execvp failed, reason: %s\n", strerror(errno));
}
printf("After execvp\n"); // 只有 execvp 失败时才会执行到这里
return 0;
}
execl函数
- 用路径path的程序替换当前进程,参数用列表形式传递
接口代码
#include<stdio.h>
#include<unistd.h>
int execl(const char* path,const char* arg0,...,(char*)0);
参数解释
- path: 指定要执行的可执行文件的路径–完整路径
- arg0,…: 表示可变数量的命令行参数,通常arg0为程序名,参数必须以NULL结尾
实例代码
原进程文件
#include <stdio.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
printf("当前是execl.c\n");
execl("/home/scq/study/gcc/009/execl_test",argv[1],argv[2],NULL);
printf("不会执行到这里\n");
return 0;
}
替换目的进程文件
#include <stdio.h>
int main(int argc,char* argv[])
{
printf("这是execl_test.c\n");
for(int i = 0; i < argc; i++)
printf("参数成员依次为%s\n",argv[i]);
return 0;
}
结果展示
当前是execl.c
这是execl_test.c
参数成员依次为aaa
参数成员依次为bb
execlp函数
- execl加强版,不需要指明完整路径,execlp会通过环境变量查找file
接口代码
#include<stdio.h>
#include<unistd.h>
int execlp(const char* file,const char* arg0,...,(char*)0);
参数解释
- file: 指定要执行的可执行文件的名称/路径
- arg0,…: 表示可变数量的命令行参数,通常arg0为程序名,参数必须以NULL结尾
实例代码
将子进程替换为pwd进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if (pid == -1)
printf("子进程创建失败\n");
else if (pid == 0)
{
printf("当前在子进程\n");
execlp("pwd", "-al", NULL);
printf("execlp执行完毕\n"); // 不会执行到这里
}
else
{
printf("开始等待子进程执行完毕\n");
wait(NULL);
printf("父进程执行完毕\n");
}
exit(0);
}
// 结果展示
/**
开始等待子进程执行完毕
当前在子进程
/home/scq/study/gcc/009
父进程执行完毕
*/
execle函数
execl的魔改版,和execl类似,但允许指定新的环境变量envp
接口代码
#include<stdio.h>
#include<unistd.h>
int execle(const char* path,const char* arg0,...,(char*)0, char* const envp[]);
参数解释
- path: 指定要执行的可执行文件的名称/路径
- arg0,…: 表示可变数量的命令行参数,通常arg0为程序名,参数必须以NULL结尾
- envp: 指定环境变量数组,替换当前进程的环境变量
实例代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
// 环境变量数组,必须以NULL结尾
char *envp[] = {"PATH=/tmp", "USER=scq", NULL};
pid_t pid;
pid = fork();
if (pid < 0)
printf("子进程创建失败\n");
else if (pid == 0)
{
printf("子进程创建成功\n");
if (execle("/usr/bin/env", "env", NULL, envp) < 0)
{
perror("execle失败");
return -1;
}
}
else
{
wait(NULL);
printf("父进程执行完毕\n");
}
exit(0);
}
// 结果展示
/**
子进程创建成功
PATH=/tmp
USER=scq
父进程执行完毕
*/
/** env
SHELL=/bin/bash
COLORTERM=truecolor
TERM_PROGRAM_VERSION=1.93.1
...
*/
execv函数
与execl类似,但是参数以数组形式传递
接口代码
#include<stdio.h>
#include<unistd.h>
int execv(const char* path,char* const argv[]);
参数解释
- path: 要执行的程序的路径
- argv: 命令行参数数组,argv[0] 通常是程序名,必须以NULL结束
实例代码
#include <stdio.h>
#include <unistd.h>
int main()
{
char *const argv[] = {
"/bin/bash", // 指定要执行的程序路径,这里是 /bin/bash
"-c", // 表示后面的字符串是 bash 命令
"ls -l", // 要执行的命令(在 bash 中运行的命令):列出当前目录下文件的详细信息
NULL // 以 NULL 结尾的指针数组,用来标记参数列表结束
};
execv(argv[0], argv);
return 0;
}
execvp函数
- execlp的强化版,参数变成数组形式
- 根据环境变量查找file
接口代码
#include<stdio.h>
#include<unistd.h>
int execvp(const char* file,char* const argv[]);
参数解释
- file: 可执行文件名称或路径
- argv: 命令行参数数组,必须以NULL结束
实例代码
#include <unistd.h>
int main()
{
char *argv[] = {
"ls", // 可执行命令
"-al", // 可执行命令参数
"/etc/passwd", // 执行命令的执行路径
NULL};
execvp("ls", argv);
return 0;
}
execve函数
- execle的强化版,参数变成数组的形式
接口代码
#include<stdio.h>
#include<unistd.h>
int execve(const char* file,char* const argv[],char* const envp[]);
参数解释
- filename: 要执行的程序的路径,例如**/bin/ls**
- argv: 参数列表,必须以NULL结束,**argv[0]**通常是程序名,后面的元素是传递给程序的参数
- envp: 环境变量列表,也必须以NULL结束,这个参数允许传递新的环境变量
实例代码
原进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char *envp[] = {
"变量1 = 111",
"变量2 = 222",
NULL};
char *argv_send[] = {
"./execve_test",
"参数一",
"参数二",
NULL};
execve("./execve_test", argv_send, envp);
printf("替换进程,打印环境变量");
return 0;
}
替换运行目的进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
extern char **environ; // 可执行程序当前的环境变量
int main(int argc, char *argv[])
{
printf("开始打印当前环境变量\n");
for (int i = 0; environ[i] != NULL; i++)
// \r (回车,Carriage Return) 是将光标移动到当前行的开头,不向下移动
printf("environ[%d]:%s\r\n", i, environ[i]);
printf("开始打印当前参数\n");
for (int i = 0; i < argc; i++)
printf("argv[%d]:%s\r\n", i, argv[i]);
return 0;
}
输出结果
开始打印当前环境变量
environ[0]:变量1 = 111
environ[1]:变量2 = 222
开始打印当前参数
argv[0]:./execve_test
argv[1]:参数一
argv[2]:参数二
exit/_exit函数
- exit函数,使程序正常终止并且返回父进程状态,如果缓冲内有内容,会进行一次写操作,会把缓冲区内容写出去
- _exit函数,使程序正常终止并且返回父进程状态,直接退出,不会对缓冲区内容处理
接口代码
#include <stdlib.h>
int exit(int status);
int _exit(int status);
abort函数
- 异常终止一个程序
接口代码
#include <stdlib.h>
void abort(void);
实例代码
#include <stdlib.h>
#include <stdio.h>
int main()
{
abort();
exit(EXIT_SUCCESS);
}
kill函数
- 通过kill函数杀死另一个进程
接口代码
#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid,int sig);
参数解释
- pid: 杀死进程PID
- sig: 要发送的信号
实例代码
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
pid_t child;
int status, retval;
if ((child = fork()) < 0) {
perror("fork");
exit(EXIT_FAILURE);
}
if (child == 0) {
// 子进程
sleep(1000);
exit(EXIT_SUCCESS);
} else {
// 父进程,非阻塞检查子进程是否结束
if ((waitpid(child, &status, WNOHANG)) == 0) {
retval = kill(child, SIGKILL); // 强制杀死子进程
if (retval) {
puts("kill failed\n");
perror("kill");
} else {
printf("%d killed\n", child);
}
waitpid(child, &status, 0); // 阻塞等待子进程结束
}
}
exit(EXIT_SUCCESS);
}