Linux_进程

基本概念

进程

进程是系统分配资源的最小单位。在Linux系统中,PCB(Process Control Block)是进程的核心,它记录了进程的基本信息和运行状态。这些内容是记录在一个名叫task_struct的结构体当中。
PCB包含大量的信息,但一般来说关注的只有:

  • 进程标识符(pid):用于唯一标识一个进程
  • 父进程标识符(ppid):父进程的pid
  • 进程状态(state):进程状态,如运行、等待、睡眠等
  • 进程优先级:表示进程执行的优先级,在Linux下由pricenice两个值决定

父子进程

一个进程可以创建另一个进程,它就成了它创建出的进程的父进程,子进程会直接复制父进程的PCB,在修改这个PCB之前,两者使用的其实就是同一个PCB。在子进程修改内容后,就拥有了独立的PCB。

页表

当进程的PCB被创建时,会同步创建其页表项,页表中记录了当前进程的虚拟地址对物理地址的映射。
父子进程的PCB空间几乎完全相同,所以其虚拟地址也相同,但会被不同的页表项映射到不同的物理地址上。所以看上去好像是一个地址出现了两个变量,实际各自拥有不同的物理地址。

进程状态

R

Running/Runnable:表示运行态或者就绪态
进程正在占用CPU执行,或者已经具备运行条件,正在等待系统分配CPU资源以便运行。在Linux中,使用TASK_RUNNING表示这种状态。

S

Sleeping:睡眠状态,有时也称为可中断的等待状态。
进程正在等待某个事件的发生(如,等待I/O操作完成,等待信号量等),此时进程可以被一些信号唤醒。

D

Disk Sleep:磁盘睡眠状态,有时也称为不可中断的等待状态。
进程正在进行某些不能中断的操作(如正在进行磁盘I/O),此时进程不能被任何信号唤醒。这种状态非常短暂,因为内核需要保护这些操作的完整性。

T

Stopped/Traced:暂停/停止状态,跟踪状态
进程被某种信号(如SIGSTOP)停止运行,或者正在被调试器跟踪,此时进程就是T状态,可以使用一些信号(如SIGCONT)让其继续运行。

Z

Zombie:僵死状态(僵尸状态)
进程已经结束,但是其父进程尚未读取其退出状态,此时,进程的内核数据已经无效了,但依然存在,这样的状态叫僵尸状态,父进程回收完毕后,退出僵尸状态。


孤儿进程
当父进程先于子进程结束时,此时子进程就成了孤儿进程,孤儿进程会被1号进程收养。


进程相关命令

top

Linux下的任务管理器,可以通过它进行一些对进程的查阅操作:

P 按CPU使用率排序
M 按内存使用率排序
T 按运行时间排序
k 可以输入pid杀死指定进程
b 高亮显示正在运行的进程
h 帮助菜单
q 退出

ps

静态查询进程信息,默认只显示当前进程和自己的子进程

常用参数:
-e :显示所有进程的基本信息(PID、创建命令)
-f :显示父进程的pid(ppid)
-l :显示PRI和NI值(PRI越低优先级越高,NI是对PRI的微调,取值范围是[-20,20])
aux :查询 CPU/MEM 占用及进程状态(也就是top中现实的内容)

进程相关函数

fork

创建一个子进程
头文件:

#include <unistd.h>

函数声明:

pid_t fork(); // pid_t 是进程ID类型(通常是整型)

返回值:

如果成功,则在父进程中会返回>0的子进程的pid,子进程中会返回0,失败返回<0。

fork 函数在调用时,会生成一个新的子进程,其PCB就是父进程的PCB,其执行的先后次序和父进程间没有明确的先后关系。

父进程内存空间
+-----------------+
| 代码段          |
| 数据段          | <-- fork() 发生时复制整个空间
| 堆栈段          |
+-----------------+
       ↓
子进程内存空间
+-----------------+
| 代码段 (相同)    |
| 数据段 (初始相同)| 
| 堆栈段 (初始相同)|
+-----------------+

执行流程的分离:

pid_t pid = fork();  // 分裂点

// 从这行开始,两个进程并行执行相同代码
// 但通过 pid 的值区分执行路径

if (pid == 0) {
    // 只有子进程会执行这里
    do_child_work();
} else if (pid > 0) {
    // 只有父进程会执行这里
    do_parent_work();
} else {
    // 错误处理
}

经典错误示例:

// 错误用法!
if (fork() == 0) {
    printf("Child\n");
} else {
    printf("Parent\n");  // 实际父进程也会执行这里
}

不检查返回值的后果:

#include <stdio.h>
#include <unistd.h>

int main() {
    fork();  // 分裂点,但不保存返回值

    // 以下代码父子进程都会执行!
    printf("Hello from PID=%d\n", getpid());
    return 0;
}

正常例子:

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();  // 创建子进程

    if (pid < 0) {
        perror("fork failed");
        return 1;
    }

    if (pid == 0) {
        // 子进程代码
        printf("Child:  My PID = %d, Parent PID = %d\n", getpid(), getppid());
    } else {
        // 父进程代码
        printf("Parent: My PID = %d, Child PID = %d\n", getpid(), pid);
    }

    return 0;
}

进阶示例:父子进程协作

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        // 子进程:计算1+2+...+5
        int sum = 0;
        for (int i = 1; i <= 5; i++) sum += i;
        printf("Child: Sum = %d\n", sum);
        _exit(0);  // 子进程直接退出
    } else {
        // 父进程等待子进程结束
        int status;
        waitpid(pid, &status, 0);  // 阻塞等待
        if (WIFEXITED(status)) {
            printf("Parent: Child exited with status %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

输出:

Child: Sum = 15
Parent: Child exited with status 0

正确的分支用法:

pid_t pid = fork();

if (pid == 0) {
    // 子进程专属代码
    do_child_task();
    exit(0);  // 子进程结束后退出
} else if (pid > 0) {
    // 父进程专属代码
    do_parent_task();
    wait(NULL);  // 等待子进程
} else {
    // 错误处理
}

getpid/getppid

这两个函数头文件都是#include <unistd.h>,分别可以取出当前进程的pid和父进程的pid。
结合fork的例子:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    printf("[原始进程] PID=%d, PPID=%d\n", getpid(), getppid());
    
    pid_t pid = fork();  // 创建子进程
    
    if (pid < 0) {
        perror("fork失败");
        return 1;
    }
    
    if (pid == 0) {
        // 子进程代码
        printf("[子进程] 我的PID=%d, 父进程PID=%d\n", getpid(), getppid());
        sleep(1);  // 等待父进程可能结束
        printf("[子进程] 1秒后父进程PID=%d\n", getppid());
    } else {
        // 父进程代码
        printf("[父进程] 我的PID=%d, 创建的子进程PID=%d\n", getpid(), pid);
        
        // 父进程立即退出(不等待子进程)
        _exit(0);  // 使用 _exit 避免刷新缓冲区
    }
    
    return 0;
}

输出:

[原始进程] PID=1234, PPID=5678   # 原始进程信息(父进程是Shell)
[父进程] 我的PID=1234, 创建的子进程PID=1235
[子进程] 我的PID=1235, 父进程PID=1234
[子进程] 1秒后父进程PID=1       # 父进程退出后,子进程被init/systemd收养

注意
当父进程先退出时,子进程会成为孤儿进程
孤儿进程会被 init 进程(PID=1)或 systemd 收养
被收养后,子进程的 getppid() 将返回 1

当从终端运行程序时,Shell 是程序的父进程
使用 ps -ef 命令可以查看完整的进程树:

UID        PID  PPID  C STIME TTY          TIME CMD
user      5678  3456  0 10:00 pts/0    00:00:00 bash        # Shell
user      1234  5678  0 10:01 pts/0    00:00:00 ./a.out     # 我们的程序
user      1235  1234  0 10:01 pts/0    00:00:00 ./a.out     # 子进程

常见问题:

Q:为什么父进程退出后,子进程的 PPID 会变成 1?
A:这是 Unix/Linux 的设计机制。当进程失去父进程时,系统会让 PID=1 的
 init/systemd 进程收养所有孤儿进程,确保进程能被正确回收

Q:getpid() 返回值会变化吗?
A:不会。进程的 PID 在其生命周期内保持不变。只有调用 exec() 系列函数后,
进程内容会被替换,但 PID 不变。

wait

等待子进程执行
头文件:

#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h> // 用于 exit

函数声明:

pid_t wait(int* status);

过程

wait调用时,如果子进程已经结束,则直接抓取其状态码,如果子进程还没结束,则会阻塞等待到子进程结束后,再抓取状态码。
参数是一个出参,带出了子进程的状态码,传入NULL则表示不在乎子进程的结束状态。子进程结束后,如果不进行wait操作回收子进程的资源,则子进程就会陷入僵尸状态。
※子进程的结束状态可以是子进程的返回值,或者是使用exit函数传入的值。wait接收到的值是该值的256倍。直接右移8位就可以得到原值,系统也提供了WEXITSTATUS宏,以方便还原原值。

例子:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>  // 用于 exit

int main() {
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("fork失败");
        exit(1);
    }
    
    if (pid == 0) {
        // 子进程代码
        printf("子进程 %d 运行中...\n", getpid());
        sleep(2);
        printf("子进程退出\n");
        exit(42);  // 设置退出码为42
    } else {
        // 父进程代码
        printf("父进程 %d 等待子进程 %d\n", getpid(), pid);
        
        int status;
        pid_t terminated_pid = wait(&status);  // 阻塞等待
        
        if (terminated_pid == -1) {
            perror("wait失败");
            exit(1);
        }
        
        printf("子进程 %d 已终止\n", terminated_pid);
        
        if (WIFEXITED(status)) {
            printf("正常退出,退出码: %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("被信号终止,信号: %d\n", WTERMSIG(status));
        }
    }
    
    return 0;
}

输出:

父进程 1234 等待子进程 1235
子进程 1235 运行中...
子进程退出
子进程 1235 已终止
正常退出,退出码: 42

状态解析宏

宏	                说明
WIFEXITED(status)	子进程正常退出时为真
WEXITSTATUS(status)	获取子进程退出码(仅当 WIFEXITED 为真)
WIFSIGNALED(status)	子进程因信号终止时为真
WTERMSIG(status)	获取导致终止的信号编号(仅当 WIFSIGNALED 为真)
WIFSTOPPED(status)	子进程被暂停时为真
WSTOPSIG(status)	获取导致暂停的信号编号

waitpid

和wait的区别仅在于它会等待一个指定pid的子进程。函数声明:

pid_t waitpid(pid_t pid, int* status, int option);

参数、返回值

参数1是要等待的pid
参数2和wait一致
参数3一般给0,表示默认配置。

返回值同wait

非阻塞等待示例:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        sleep(5);
        exit(0);
    }
    
    // 父进程:非阻塞等待
    printf("父进程开始等待(非阻塞模式)\n");
    int status;
    pid_t result;
    
    while (1) {
        result = waitpid(pid, &status, WNOHANG);
        
        if (result == -1) {
            perror("waitpid失败");
            break;
        } else if (result == 0) {
            printf("子进程尚未结束,继续等待...\n");
            sleep(1);
        } else {
            printf("子进程 %d 已结束\n", result);
            break;
        }
    }
    
    return 0;
}

使用进程函数完成的一个父子进程间的通讯:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
	int a; //创建子进程前的资源
	int ret = fork(); //复制时,父子进程拥有的是一个a
	if (ret < 0) //失败判断
	{
		perror("fork error ");
		return 0;
	}
	else if(ret > 0) //返回值大于零,表示当前执行的是父进程
	{
		a = 8;
		printf("I'm father %d\n", a);
		printf("I'm %d, my son is %d\n", getpid(), ret);
		wait(&a); //这里可以用a接取子进程的退出状态,使用WEXITSTATUS还原其值
		printf("my son returns %d\n", WEXITSTATUS(a));
	}
	else
	{
		a = 10;
		printf("I'm son %d\n", a);
		printf("I'm %d, my father is %d\n", getpid(), getppid());
		//return 10;
		exit(10); //这两种方法都可以表示子进程的退出状态为10
	}
	return 0;
}

exec系列函数

表示执行一个系统命令,这个函数会直接切换到对应命令的PCB执行。
execl、execle、execlp
函数声明:

int execl(const char* path, const char* cmd, ...);
int execle(const char* path, const char* cmd, ..., const char* envp[]);
int execlp(const char* path, const char* cmd, ...);

path表示对应命令所在的文件路径,cmd一串命令执行的各个字段,以一个NULL结尾。evnp是环境变量。
其中execl不继承任何环境变量,execle使用传入的环境变量,execlp使用默认的环境变量。
execv、execve、execv
函数声明:

int execv(const char* path, const char* cmd[]);
int execve(const char* path, const char* cmd[], const char* envp[]);
int execvp(const char* path, const char* cmd[]);

和上面的一致,只是把不定参换成了一个命令的数组,这个数组一样用NULL结尾。

待补充

.......补充

练习

通过exec系列函数完成一个终端

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

#define STRNUM 64

int divideStr(char* src, char dst[][64])
{
	char* start = src;
	int dpos = 0;
	char* pos = strchr(src, ' ');
	while (pos)
	{
		strncpy(dst[dpos], start, pos - start);
		dst[dpos][pos - start] = '\0';
		dpos++;
		
		start = pos + 1;
		pos = strchr(start, ' ');
	}
	strcpy(dst[dpos], start);
	
	return dpos + 1;
}

void copyCmd(char src[][64], char* dst[], int n)
{
	int i;
	for (i = 0; i < n; i++)
	{
		dst[i] = src[i];
	}
	dst[i] = NULL;
}

int main()
{
	while (1)
	{
		char alldata[STRNUM][64] = { 0 };
		char* arg[STRNUM];
		
		printf("$ ");
		fflush(NULL);
		char userinput[256] = { 0 };
		scanf("%255[^\n]%*c", userinput);
		
		int ret = fork();
		if (ret < 0)
		{
			perror("fork error ");
		}
		else if (ret > 0)
		{
			wait(NULL);
		}
		else
		{
			ret = divideStr(userinput, alldata);
			copyCmd(alldata, arg, ret);
			ret = execvp(arg[0], arg);
			if (ret < 0)
			{
				perror("exec error ");
			}
		}
	}
	
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值