Linux - 进程 - 进程创建

一. 系统调用

CPU 既可以运行在用户态下,也可以运行在内核态下, 所有标准的 Unix 内核都利用了内核态和用户态.

当一个程序在用户态下执行时, 它不能直接访问内核数据结构或内核的程序. 然而, 当应用程序在内核态下运行时, 这些限制不再有效. 每种 CPU 模型都为从用户态到内核态的转换提供了特殊的指令, 反之亦然 (即从内核态到用户态). 一个程序执行时, 大部分时间都处在用户态下, 只有需要内核所提供的服务时才切换到内核态. 当内核满足了用户程序的请求后, 它让程序又回到用户态下.

内核本身并不是一个进程, 而是进程的管理者. 进程/内核模式假定: 请求内核服务的进程使用所谓系统调用 (system call) 的特殊编程机制. 每个系统调用都设置了一组识别进程请求的参数, 然后执行与硬件相关的 CPU 指令完成从用户态到内核态的转换.

如下图所示, 用户态进程在对内核发出系统调用请求的时候, 内核调用系统调用处理程序让进程从用户态切换为内核态, 并执行系统调用接口.

二. getpid() && getppid()

每个进程都有一个非负整型表示的唯一进程 ID, 虽然是唯一的, 但是进程 ID 是可复用的. 当一个进程终止后, 其进程 ID 就成为复用的候选者. 大多数 UNIX 系统实现延迟复用算法, 使得赋予新建进程的 ID 不同于最近终止进程所使用的 ID. 这用于防止将新进程误认为是使用同一 ID 的某个已终止的先前进程.

用户程序想要获取进程 ID, 需要调用系统调用 getpid(), 与 getpid() 类似的系统调用 getppid() 用于获取发出该系统调用进程的父进程 ID.

通过如下一段代码对上述两个接口进行测试.

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    while (1)
    {
        printf("I am a process.....:PID:%d, PPID:%d\n", getpid(), getppid());
        sleep(1);
    }
    return 0;
}

当运行生成的可执行程序后, 该进程的 PID 和 PPID 被循环打印。

通过 ps 命令查看该进程的信息, 即可发现通过 ps 命令得到的进程 PID 和 PPID 与使用系统调用函数 getpidgetppid 所获取的值相同.

注意: 系统调用 getpid() 和 getppid() 的返回值类型都为 pid_t, pid_t 本质是对 int 的 typedef, 所以可以认为两个系统调用接口的返回值类型就为 int. 使用 pid_t 的时候需要包含 <sys/types.h> 头文件, 因为在该头文件中有对 pid_t 的宏定义.

三. fork()

Unix 操作系统紧紧依赖进程创建来满足用户的需求, 例如, 只要用户输入一条指令, shell 进程就创建一个新进程, 新进程执行输入的指令.

一个现有的进程可以调用 fork() 创建一个新进程, 由 fork() 创建的新进程被称为子进程 (child process).

通过如下一段代码对 fork() 进行测试.

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    fork();
    while (1)
    {
        printf("I am a process.....PID:%d, PPID:%d\n", getpid(), getppid());
        sleep(1);
    }
    return 0;
}

当运行生成的可执行程序时, 发现子进程与父进程分别循环打印自己的 PID 和 PPID.

出现这样的现象的原因是, 父进程 fork() 创建子进程后, 子进程与父进程共享同一份正文代码.

fork() 的返回值

fork() 调用成功时, 父进程的返回值是新建子进程的 ID, 子进程的返回值是 0; 而当调用 fork() 失败时, 父进程的返回值是 -1, 没有子进程被创建.

基于 fork() 的这一特性 (fork() 被调用一次, 但返回两次), 用 if 语句进行分流, 让父进程和子进程执行不同的代码块, 完成不同的任务.

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t ret = fork();
    if (ret < 0) {
        perror("fork");
        return 1;
    }
    else if (ret == 0) {
        while (1) {
            printf("I am child process.....:PID:%d, ret:%d\n", getpid(), ret);
            sleep(1);
        }
    }
    else {
        while (1) {
            printf("I am father process.....:PID:%d, ret:%d\n", getpid(), ret);
            sleep(1);
        }
    }
    return 0;
}

当运行生成的可执行程序时, 发现父进程会进入到 else 语句的循环打印当中, 打印父进程 ID 和父进程接收到的 fork() 返回值; 而子进程会进入到 else if 语句的循环打印当中, 打印子进程 ID 和子进程接收到的 fork() 返回值.

注意: 一般来说,在 fork() 之后是父进程先执行还是子进程先执行是不确定的, 这取决于内核所使用的调度算法.

写时复制 (Copy-On-Write)

在早期的 UNIX 系统中, 创建进程很简单. 调用 fork() 时, 父进程由用户态切换为内核态.

内核会为新创建的子进程分配 PCB (task_struct) 并对其进行填充, 并将父进程的部分数据结构拷贝给子进程, 比如复制父进程的页表, 比如将父进程的地址空间中的内容按页复制到子进程的地址空间中, 糟糕的是, 这种按页复制方式是十分耗时的.

现代 Unix 系统采取了更优的实现方式. 在现代 Unix 系统如 Linux 中, 采用了写时复制 (Copy-On-Write, COW) 的技术, 而不是对父进程部分数据结构进行整体复制.

写时复制是一种基于惰性算法 的优化策略, 这样的策略是为了避免复制时的系统开销. 其前提假设为: 如果有多个进程要读取 它们所共有的那部分资源, 那么复制行为是不必要的, 每个进程只要保存一个指向该资源的指针即可. 只要没有一个进程修改自己的 "副本", 每个进程就好像独占了那部分资源, 从而避免了复制带来的开销. 如果某个进程想要修改 (写入) 自己的那份资源 "副本", 就会开始复制该资源, 并把副本提供给这个进程. 复制过程对进程而言是 "透明" 的. 这个进程后面就可以反复修改其持有的副本, 而其他进程还是共享原来那份没有修改过的资源. 这就是 "写时复制" 这个名称的由来: 只有在对资源进行写入时才执行复制.

写时复制的主要好处在于: 如果进程从未修改资源, 则都不需要执行复制. 一般来说, 惰性算法的好处就在于它们会尽量延迟代价较高的操作, 直到必要时才执行.

在使用虚拟内存的场景下, 写时复制是以页为基础执行的. 所以, 只要进程没有修改其全部地址空间, 就不需要复制整个地址空间. 在 fork() 调用结束后, 父进程和子进程都以为自己有唯一的地址空间, 实际上它们共享父进程的地址空间, 这些以页为单位存放的父进程地址空间又会被其他的进程共享.

写时复制在内核中的实现非常简单. 存放进程地址空间的这些页被标记为只读 , 如果有进程试图修改某个页, 就会产生缺页中断. 内核处理缺页中断的处理方式就是对该页进行一次 "透明" 的复制. 这时, 会对该页中的引用计数进行--, 每触发一次缺页中断, 引用计数就会--, 直到引用计数被减至 0, 清空该页的写时复制属性, 表示该页不再被共享.

对于调用 fork() 创建进程的场景, 写时复制有着更大的优势. 比如当一个进程要执行一个不同的程序时, 这对 shell 进程来说是常见的情况. 在这种情况下, 子进程被 fork() 创建后后立即调用 exec 系列函数, 在用 exec 进行进程程序替换时, 子进程的地址空间会被交换出去 , 若不采用写时复制, 则会在调用 fork() 时把父进程的地址空间中的内容按页复制到子进程的地址空间, 这样的行为没有意义, 且增加了系统额外的开销, 写时复制可以对这种情况进行优化.

相关推荐
康熙38bdc1 小时前
Linux 进程优先级
linux·运维·服务器
hhzz1 小时前
Linux Shell编程快速入门以及案例(Linux一键批量启动、停止、重启Jar包Shell脚本)
android·linux·jar
只是有点小怂1 小时前
parted是 Linux 系统中用于管理磁盘分区的命令行工具
linux·运维·服务器
三枪一个麻辣烫2 小时前
linux基础命令
linux·运维·服务器
cuisidong19972 小时前
如何在 Kali Linux 上安装 Google Chrome 浏览器
linux·运维·chrome
光通信学徒3 小时前
ubuntu图形界面右上角网络图标找回解决办法
linux·服务器·ubuntu·信息与通信·模块测试
南种北李3 小时前
Linux自动化构建工具Make/Makefile
linux·运维·自动化
小飞猪Jay3 小时前
面试速通宝典——10
linux·服务器·c++·面试
暗恋 懒羊羊4 小时前
Linux 生产者消费者模型
linux·开发语言·ubuntu
安红豆.5 小时前
Linux基础入门 --13 DAY(SHELL脚本编程基础)
linux·运维·操作系统