【Linux系统编程】进程概念(二)进程的概念和基本操作
【Linux系统编程】进程概念(二)进程理解、父子进程
- 1. 进程概念
 - 1.1 进程与PCB
 - 1.2 深入理解PCB(task_struct)
 
- 2 进程基本操作
 - 2.1 查看进程
 - 2.2 查看进程状态信息
 - 2.3 获取PID和PPID
 - 2.4 如何创建进程(子进程)
 - 2.4.1 fork
 - 2.4.2 fork的返回值
 
- 2.5 为什么新建文件默认是新建在当前路径?
 
1. 进程概念
1.1 进程与PCB
通俗地讲,一个已经加载到内存中并正在运行(或等待运行)的程序,我们称之为进程,也常被称为任务。
从操作系统内核的角度分析,进程本质由两部分组成:
- 你自己的代码和数据:即来自那个静态的程序二进制文件。
 - 内核的进程控制块PCB(process control bolck):一个用于描述和管理该进程的内核数据结构。
 
核心机制:“先描述,再组织”
- 描述:操作系统需要管理进程,第一步就是“描述”它。它会为每一个进程创建一个专门的数据结构,用来记录该进程的所有属性信息。这个数据结构就是进程控制块(PCB)。因此,PCB是进程属性的集合体。
 - 组织:当系统中存在多个进程时,操作系统会通过数据结构将这些PCB有效地组织起来,以便对这些进程进行增删查改等操作。
 
1.2 深入理解PCB(task_struct)
PCB是一个通用术语,在Windows、Linux、MacOS等操作系统中都有对应的实现。在Linux中,这个关键的数据结构被具体命名为task_struct。
- 它是什么? task_struct是Linux内核中定义的一个结构体,是PCB的具体实现。
 - 它在哪? 它被加载在内存(RAM)中。
 - 它做什么? 它完整地描述了一个进程的全部信息。
 
这个结构体非常复杂,但大体上包含以下几类关键信息:
- 标识符:描述本进程的唯一标识符,用来区别其他进程。
 - 状态:任务状态,退出代码,退出信号等。
 - 优先级:相对于其他进程的优先级。
 - 程序计数器:程序中即将被执行的下一条指令的地址。
 - 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针。
 - 上下文数据:进程执行时处理器的寄存器中的数据。
 - I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
 - 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记帐号等。
 - 其他信息。
 
2 进程基本操作
2.1 查看进程
ls /proc
 
这些数字都是进程的标识符

2.2 查看进程状态信息
ps axj
 
第一行的PID就是该进程的标识符,PPID就是该进程的父进程的标识符。

2.3 获取PID和PPID
头文件:#include< sys/types.h >、#include< unistd.h >
获得该进程的标识符的函数 pid_t getpid(void);
获得该进程的父进程的标识符的函数 pid_t getppid(void);
以下面的代码为示例
#include <stdio.h>      
#include <sys/types.h>      
#include <unistd.h>      int main()      
{                                                                                                                                                                                 while(1)    {           pid_t pid = getpid();    pid_t ppid = getppid();    printf("我是一个进程:pid:%d, ppid: %d\n", pid, ppid);    sleep(1);                                                  }                                                              return 0;        
}                
 
可以知道该进程的标识符是19190,其父进程的标识符是17553
 
 ps axj | head -1 是拿到第一行
 ps axj | grep process 是显示process的进程状态信息
 ps axj | head -1 && ps axj | grep process 就是两个命令都执行
经过查询,发现PID和PPID确实是跟程序输出的一样
 
那么该进程的父进程到底是谁呢?
通过查询,我们发现该进程的父进程是bash(命令行解释器),所以bash本身也是一个进程,并且我们在bash上输入的命令ls、pwd等其实是bash进程创建的子进程。

我们自己能不能通过代码的方式也创建子进程呢?
2.4 如何创建进程(子进程)
2.4.1 fork
头文件:#include< unistd.h >
创建子进程的函数:pid_t fork(void);
以下面的代码为例
#include <stdio.h>      
#include <sys/types.h>      
#include <unistd.h>      int main()      
{      printf("I am a process:pid:%d, ppid: %d\n", getpid(), getppid());                           sleep(1);                         // 创建子进程                     fork();                           while(1)                                                    {                                                           pid_t pid = getpid();                                   pid_t ppid = getppid();                                 printf("我是一个进程:pid:%d, ppid: %d\n", pid, ppid);    sleep(1);                                               }                                                           return 0;                                                   
}  
 
我们发现while循环中的代码会执行两次,这是为什么?
还记得我们执行fork函数创建了一个子进程吗?其实fork函数创建的子进程会有自己的task_struct,但代码和数据是和父进程共享的,所以输出的pid和ppid都是自己task_struct中的pid和ppid。

查询之后发现process的父进程是bash,子进程是我们fork创建的子进程,且我们创建的子进程的父进程是process。

 那子进程有什么用呢?目前看来子进程执行的代码跟父进程是一样的,好像没什么用啊。
凡是创建子进程,都是为了完成任务。
所以我们就要说说fork的返回值了。
2.4.2 fork的返回值
如果系统调用fork函数创建子进程失败,就返回一个 < 0 的值;
如果系统调用fork函数创建子进程成功,就给子进程返回一个 0 ,给父进程返回子进程的标识符(PID)
我们以下面的代码为例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{printf("我是父进程:pid:%d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id < 0){return 1;}else if(id == 0){// childwhile(1){printf("I am 子进程, pid: %d, ppid: %d, id: %d\n", getpid(), getppid(), id);sleep(1);}}else{// parentwhile(1){printf("I am 父进程, pid: %d, ppid: %d, id: %d\n", getpid(), getppid(), id);sleep(1);}}                                                                                                                                                                             return 0;
}
 

 
由上面代码输出的结果,下面我们提出 3 个问题
1. 为什么给子进程返回的是0,给父进程返回的是子进程的pid?
父进程与子进程之间是1对多的,之后讲进程状态的时候会提到子进程结束后,父进程是要对子进程进行回收的,所以要给父进程返回对应子进程的pid;那fork返回的值 > 0 和 < 0 的值都返回过了,所以子进程就返回 0 了。
2. fork()函数为什么会返回两次啊?
大家想一下,fork函数返回之前是不是创建子进程已经完成了才返回的,还记得我们之前说的吗?其实fork函数创建的子进程会有自己的task_struct,但代码和数据是和父进程共享的, 所以return 返回的代码也是一样的啊,只是相对于父进程fork返回对应子进程的pid,相对于子进程fork返回0。
3. 一个id,怎么能接收两个不同的值?既 == 0,又 > 0 ?
这个问题其实上面讲的你理解了,就自然知道了,父进程和子进程同时执行两份相同的代码,但由于返回值id的不同,进入了不同的if逻辑中。
2.5 为什么新建文件默认是新建在当前路径?
以下面的代码为例,说明问题
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{fopen("test.txt", "w");while(1){printf("pid: %d\n", getpid());sleep(1);}return 0;
}
 

 可以看到默认在当前路径创建了test.txt
 
输入命令 ls /proc/当前进程的pid -l,可以看当前进程的信息,其中下面图片中的cwd后面的路径就是当前进程所在路径,如果新建文件不给路径默认就会在该路径下新建文件

下面我们把cwd中的路径改了,看是否是我说的那样。
更改cwd中路径的函数 chdir
头文件:#include < unistd.h >
函数声明:int chdir(const char* path);
path就是要更改为哪个路径。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{chdir("/home/lsb");fopen("test.txt", "w");while(1){printf("pid: %d\n", getpid());sleep(1);}return 0;
}
 

这样我们就把路径改了,但是如果下次我们代码中不调用chdir改变路径,它的路径就还是原来的默认当前路径。

