进程概念和基本操作
什么是进程
进程 = 内核数据结构 (task_struct) + 自己的程序代码和数据
Linux 下进程的基本概念、task_struct 结构体以及查看进程的方法。详细讲解了 PID、Ctrl+C 中断、当前工作路径及常用指令。重点阐述了父进程与子进程的关系,fork 系统调用的返回值含义、执行流程及数据独立性(写时拷贝机制)。通过代码示例说明了如何利用 fork 实现多进程编程。

进程 = 内核数据结构 (task_struct) + 自己的程序代码和数据
内核数据结构(struct task_struct):这是进程的'学籍信息',包含了进程的 PID、优先级、内存地址、状态等元数据,是操作系统管理进程的核心。 程序的代码和数据:这是进程的'本人',即被加载到内存中的可执行程序内容。
传统教材常说:'运行起来的程序就是进程'或'加载到内存的程序叫做进程'。但是这种说法只强调了'代码和数据加载到内存',忽略了内核为管理进程而维护的数据结构,显得过于片面,不易理解。
从软件角度讲,把你的二进制可执行程序加载到内存的过程,就叫做创造进程。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
可以在内核源代码里找到它。所有运行在系统里的进程都以 task_struct 双链表的形式存在内核里。

我们可以用指令来查看进程:ps axj | grep ./[可执行程序]
进程的信息可以通过 /proc 系统文件夹查看。 如:要获取 PID 为 1 的进程信息,你需要查看 /proc/1 这个文件夹。

ls /proc/[PID]

PID 是 Process ID(进程标识符)的缩写,是操作系统给每个正在运行的进程分配的一个唯一数字编号。 可以把 PID 理解成:进程的身份证号。
PID 可以被操作系统用作:
例如:
注意:PID 不是固定的,它是动态分配和回收的。 在系统的同一时间点,每个运行的进程都有一个唯一的 PID。 复用机制:当进程退出后,它的 PID 会被内核回收,并可以分配给新创建的进程。内核会从可用的 PID 池中按顺序分配,不会永久占用。
我们在上面图片左部分看到的数字就是 PID。

我们想要获取 PID,可以用 getpid() 这个系统调用
在 Linux/Unix 终端中,它是发送 SIGINT(中断信号,信号编号 2)的快捷键,默认行为是终止当前前台运行的进程。
作用范围:仅对当前终端的前台进程 / 进程组生效,后台运行的进程不受影响。
cwd:current work dir(当前工作路径)

为什么要有当前路径?为了支持进程访问文件时缺省路径的问题。

当前路径就是进程启动时候所处的路径!

大多数进程信息同样可以使用 top 和 ps 这些用户级工具来获取。

如果改变一个进程的当前工作路径,那么它在新建文件的时候,就一定会改变新建文件的位置
我们来写一段循环输出进程 ID 的代码。
#include <stdio.h>
#include <unistd.h>
int main() {
while(1) {
printf("PID: %d\n", getpid());
sleep(1);
}
return 0;
}

我们在上述代码中加上一句 chdir("/tmp")。
此时工作路径就被改变了,也证明了文件建在哪个路径取决于当前进程的工作路径(cwd)。


我们可以通过 getcwd 判断路径是否改变。



里面每一个选项的详细介绍,我们到后面再讲解。
我可以利用这个系统调用,找到指定的进程:

我们发现上面有一列信息的表头叫做 TTY,TTY 是用来表示该进程是在哪个窗口启动的。

每个进程除了会记录自己的 PID,还会记录自己父进程的 PID。

我们来写一个程序看一下:

运行之后发现,父进程的 PID 不会改变。

那么此时的这个父进程是谁?

bash 是一个命令行解释器,它本身也是一个进程!
每一次登录,系统都会自动提供 bash,来进行命令行服务。
我们运行 bash 之后发现,bash 本身是一个内部具有死循环的软件。

当我们用 ps 去看 bash 时,发现有些前面带'-',有些不带,为什么呢?

当你每一次远程登录 Linux 系统时,SSH 客户端都会分配一个新窗口,这种远程登录形成的 bash,叫做-bash;而手动形成的 bash 前面没有 -。
杀掉一个进程。
我们先来引入一个新的系统调用:fork 创造一个子进程。

使用一下 fork:

运行得到:

我们发现 fork 会分成父进程和子进程分别执行一遍:

我们来看一下 fork 的返回值是什么:

也就是说,如果成功的话,会把子进程的 PID 返回给父进程,返回 0 给子进程。
我们来写一个程序测试一下:

运行之后发现:为什么父进程和子进程都运行了?fork 怎么能既大于 0,又等于 0 呢?

我们以前学习的代码,不可能两个死循环同时在跑,以前的代码都叫做单循环分支。但是我们现在有了父子两个进程,所以同时存在两个死循环。
我们用 ps 查看一下这两进程,发现这两个进程同时都在运行。

所以,我们可以通过 fork,让同一份代码,做不同的事情。
创建子进程:内存中就多了一个进程,操作系统中多了 PCB。子进程会以父进程为模板,一般会把父进程的结构与变量,都拷贝一份给子进程。但是子进程中也有自己特有的部分:自己独立的 PID,PPID 等。所以拷贝到子进程中后,会对子进程中的个别属性作出修改。
当用 fork 创建一个子进程时,没有把数据和代码重新加载的过程。所以此时的拷贝,是浅拷贝。子进程也会指向父进程的代码和数据,他们共享代码。但是子进程只能执行 fork 之后的代码。


子进程只能有一个父进程。
fork 既是一个系统调用,也是一个函数。只要是函数,就有代码块,就有子过程。 调用函数时,是需要进入函数内部。fork 准备 return 时,子进程已经创建好了,此时 fork 内部有父、子两个进程,也已经被操作系统管理起来了。 return 是语句,此时父进程和子进程都需要 return(return 的代码被共享),各自 return 一次,返回两次。

对问题 3 的回答这里只做原理性说明。 进程具有很强的独立性---任何一个进程的崩溃都不会影响另一个进程。
问题:代码共享时,数据是怎么处理的?

我们来写一段代码来检验这个问题:

但是我们运行了之后发现:父进程的 flag 居然不受影响!!

这说明父子进程的数据是分开的。
我们分别在父进程和子进程中取地址,看一下是否是一个变量。

我们发现,地址是一样的,说明是一个变量。那这就说明,这个地址一定不是物理地址!!那是什么地址呢??
我们先引出概念:这个地址是虚拟地址(具体会在后面的程序地址空间讲解)。

子进程继承父进程的虚拟内存空间,如果不修改数据,打印出来的地址一样。但是如果数据修改,系统会在物理内存中拷贝一个 flag,这个拷贝就叫做写时拷贝。此时子进程会通过虚拟内存机制,映射到这个新的 flag。
这个过程是操作系统自己完成的,不需要手动完成。


示例:创建多个进程。
注意:.cc 后缀表示 C++。


微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online