文章目录
- 一.进程的概念
- 进程控制块(PCB)
- 二.进程查看
- 通过指令查看进程
- 通过proc目录查看
- 进程的`cwd`和`exe`
- 获取进程pid和ppid
- 通过fork()创建子进程
一.进程的概念
进程是一个运行起来的程序,而程序是存放在磁盘的,cpu要想执行程序的指令,需要先将程序加载到内存中。
课本概念:进程是被加载到内存运行的程序。
内核观点:担当分配系统资源(CPU时间,内存)的实体。
操作系统中有着大量的进程,操作系统作为管理者,管理的其实是大量进程相关的数据,那么如何管理这些数据呢?
当二进制代码直接加载到内存时,操作系统为了更好地管理加载的程序,创建了描述该进程的数据结构。这样,操作系统只用看这个数据结构,不用管各种复杂多样的二进制代码,并且将它们组织起来进行管理。
进程控制块(PCB)
这个数据结构叫PCB(process control block),进程信息被放在其中,可以理解为进程属性的集合,在linux的PCB是task_struct
。
struct task_struct {volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */struct thread_info *thread_info;atomic_t usage;unsigned long flags; /* per process flags, defined below */unsigned long ptrace;int lock_depth; /* Lock depth */int prio, static_prio;struct list_head run_list;prio_array_t *array;//.....
}
当有一个程序被加载到内存时,操作系统会为该进程在内存中创建一个task_struct
类型的对象,并将该进程放入双链表等其他结构中。这样,操作系统对进程的管理就变为操作系统对PCB的管理,再变为操作系统对双链表等结构的增删查改等操作。
由此可以总结:进程 = 内核数据结构(PCB等)+ 可执行程序(代码+数据)
二.进程查看
通过指令查看进程
为了让进程能够一直运行方便观察,写一个死循环程序,让其每隔1秒钟打印一句话。
#include <stdio.h>
#include <unistd.h>int main()
{while(1){printf("It's a process.\n");sleep(1);}return 0;
}
随后运行它,此时该程序变成了一个进程:
接着就可以用ps
指令查看进程信息,同时配合grep
进行抓取
ps ajx | grep myprocess
得到以下结果:
可以看到系统中关于myprocess
的进程一共有两个,第一行是我们写的运行的程序,第二行是grep
命令进行抓取的进程。展示了各种信息:PPID、PID、PGID等等,这些就是PCB的一部分。
注意:task_struct
是内核数据结构,查看进程信息读取该数据,必须要通过系统调用。
通过proc目录查看
proc
是一个目录,里面存放当前系统实时的 进程信息。
ls /proc
这里的数字就是进程的PID
,由于此时已经将myprocess
进程停止,此目录并没有找到名为167647
的目录。
但是,仔细看,却有165058
,这是刚才myprocess
的父进程ID即PPID
,通过指令可以知道,该进程其实就是bash
:
再次运行myprocess
,并且通过指令得到其PID
,进入该文件夹,可以发现进程的数据显式存在文件中。
进程的cwd
和exe
查看该目录详细信息,有两个文件很瞩目
cwd
: Current Work Directory 指出该进程当前工作路径
exe
: 指出该进程可执行程序的磁盘文件
修改程序,添加一个fopen
函数
#include <stdio.h>
#include <unistd.h>int main()
{FILE* fp = fopen("1.txt", "w"); // 若不存在就创建while (1) {printf("It's a process.\n");sleep(1);}
}
这恰好就是cwd
链接的目录,说明fopen
使用了查看cwd
的系统调用。
再看exe
,此时进程运行中,直接删除其链接在磁盘中的文件,发现进程没有终止,停止进程再运行显然就会失败了。
运行程序,本质就是将其从磁盘拷贝至内存中,进程与其磁盘上对应程序没有直接关系。
获取进程pid和ppid
可以直接通过系统调用getpid()
和getppid()
得到当前进程的pid和ppid(父进程的pid),返回值为pid_t
类型,底层就是整数。
运行以下代码
#include <stdio.h>
#include <unistd.h>int main()
{while (1){printf("It's a process.\t");printf("pid:%d, ppid:%d\n",getpid(), getppid());sleep(1);}return 0;
}
可以看到打印出当前进程的pid
和ppid
。
通过ps axj | head -1; ps axj | grep 184670
进行验证,当前进程是./myprocess
且其父进程是bash
。
通过fork()创建子进程
通过man
指令查看fork()
函数细节
fork()
函数可以创建子进程,创建成功后父子进程代码共享。
若成功创建,子进程的pid
返回给父进程,0返回给子进程;
若失败,-1返回给父进程,没有子进程。
代码共享可以通过以下代码得到验证
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("before\n");fork();printf("Hello, pid:%d\n", getpid());
}
在fork()
之前的代码只执行了一次,之后的代码执行了两次,这两次分别是两个进程执行的。
创建父子进程是为了做不同的事情,一般是通过if/else
来进行分流达到的,这恰恰用到了fork()
有两个返回值的特点,下面的代码若是初见一定会迷惑。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{pid_t id = fork();// id: 0-子进程 >0-父进程if (id == 0){while(1){printf("child process, pid: %d, ppid: %d", getpid(), getppid());sleep(1);}}else{while(1){printf("father process, pid: %d, ppid: %d", getpid(), getppid());sleep(1);}}
}
利用父子进程fork()
返回值不同,达到两个死循环都在不断执行的效果:
通过指令查看,确实两个进程是父子进程关系:
下面来简要分析上面的情况,具体细节会在之后进程地址空间部分详谈。
- 为什么两个死循环会同时执行❓
上节讲过,进程 = 内核数据结构(PCB等)+ 可执行程序(代码+数据)。通过fork()
创建子进程,肯定也要给子进程创建一个独立的task_struct
,而其代码和数据指向了父进程接下来的代码和数据。子进程的大部分属性值也是由父进程拷贝而来,修改前地址不会改变。
在CPU角度,它不会管谁是父进程,谁是子进程,会在操作系统的管理下并发执行。在我们的视角下,两个死循环同时执行了。
- 为什么
fork()
返回值如此设计❓
父与子的关系是一对一或者一对多的。这样的关系导致父找子并不容易,所以创建子进程成功后需要把子进程的pid
返回给父进程,方便父进程控制子进程。
而子找父是很容易的,通过系统调用getppid()
即可。
- 为什么
fork()
会返回两次值❓
fork()
之前只有父进程,即只有父进程才能调用fork()
。fork()
内部在return
之前肯定已经将子进程创建成功,又子进程和父进程在创建成功后代码共享,那么子进程和父进程都会执行return
这条语句,这也就是为什么fork()
会返回两次值。
- 同一个变量
id
怎么会既大于0,又等于0❓
进程之间具有独立性,一个进程崩溃了,不会影响另一个进程。这里的id
是父子进程的共享数据,若父子进程对共享数据有写操作,这时操作系统会将该数据拷贝两份,这就是写时拷贝。那么此时,虽然这是同一个变量名,但实际上表示的是不同的值,那么id
出现两种情况也就不足为奇了,实际在底层的空间根本就不是一个。