• linux系统编程专题(七) 系统调用之进程


    介绍linux进程相关概念,以示例方式讲述进程的fork操作

    一、进程相关概念

    程序和进程

    程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、 设备、锁…)

    进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源,在内存中执行。(程序运行起来,产生一个进程)

    程序 → 剧本(纸) 进程 → 戏(舞台、演员、灯光、道具…) 同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)。如:同时开两个终端,各自都有一个 bash 但彼此 ID 不同。

    并发

    并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但任一个时刻点上仍只有一个进程在运行。

    例如,当下,我们使用计算机时可以边听音乐边聊天边上网。 若笼统的将他们均看做一个进程的话,为什么可以同时运行呢,因为并发。

    分时复用cpu
    分时复用 cpu

    单道程序设计

    所有进程一个一个排对执行。若 A 阻塞,B 只能等待,即使 CPU 处于空闲状态。而在人机交互时阻塞的出现是必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。

    多道程序设计

    在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。

    时钟中断即为多道程序设计模型的理论基础,并发时,任意进程在执行期间都不希望放弃 cpu。因此系统需要一种强制让进程让出 cpu 资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。

    在多道程序设计模型中,多个进程轮流使用 CPU (分时复用 CPU 资源),而当下常见 CPU 为纳秒级,1 秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。

    1s = 1000ms, 1ms = 1000us, 1us = 1000ns 1000000000 实质上,并发是宏观并行,微观串行! -----推动了计算机蓬勃发展,将人类

    引入了多媒体时代。

    CPU 和 MMU

    内存管理单元 MMU:Memory Management Unit英文缩写

    中央处理器

    中央处理器(CPU)
    内存管理单元
    内存管理单元 MMU

    进程控制块 PCB

    我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体。

    /usr/src/linux-headers-3.16.0-30/include/linux/sched.h 文件中可以查看 struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:

    • 进程 id。系统中每个进程有唯一的 id,在 C 语言中用 pid_t 类型表示,其实就是一个 非负整数。
    • 进程的状态,有就绪、运行、挂起、停止等状态。
    • 进程切换时需要保存和恢复的一些 CPU 寄存器。
    • 描述虚拟地址空间的信息。
    • 描述控制终端的信息。
    • 当前工作目录(Current Working Directory)。
    • umask 掩码。
    • 文件描述符表,包含很多指向 file 结构体的指针。
    • 和信号相关的信息。
    • 用户id和组id。
    • 会话(Session)和进程组。
    • 进程可以使用的资源上限(Resource Limit)。

    进程状态

    进程基本的状态有 5 种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。

    进程状态

    环境变量

    环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:

    1. 字符串(本质)
    2. 有统一的格式:名=值[:值]
    3. 值用来描述进程环境信息。

    存储形式:与命令行参数类似。char *[]数组,数组名 environ,内部存储字符串,NULL 作为哨兵结尾。

    使用形式:与命令行参数类似。

    加载位置:与命令行参数类似。位于用户区,高于 stack 的起始位置。

    引入环境变量表:须声明环境变量。extern char ** environ;

    常见环境变量

    按照惯例,环境变量字符串都是 name=value 这样的形式,大多数 name 由大写字母加下划线组成,一般把 name 的部分叫做环境变量,value 的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:

    PATH

    可执行文件的搜索路径。ls 命令也是一个程序,执行它不需要提供完整的路径名/bin/ls, 然而通常我们执行当前目录下的程序 a.out 却需要提供完整的路径名./a.out,这是因为 PATH 环境变量的值里面包含了 ls 命令所在的目录/bin,却不包含 a.out 所在的目录。PATH 环境变量的值可以包含多个目录,用:号隔开。在 Shell 中用 echo 命令可以查看这个环境变量的值:

    root@VM-4-5-ubuntu:~# echo $PATH
    /usr/local/jdk-18.0.1.1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
    
    • 1
    • 2

    SHELL

    当前 Shell,它的值通常是/bin/bash。

    root@VM-4-5-ubuntu:~# echo $SHELL
    /bin/bash
    
    • 1
    • 2

    TERM

    当前终端类型,在图形界面终端下它的值通常是 xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。

    root@VM-4-5-ubuntu:~# echo $TERM
    xterm-256color
    
    • 1
    • 2

    LANG

    语言和 locale,决定了字符编码以及时间、货币等信息的显示格式。

    root@VM-4-5-ubuntu:~# echo $LANG
    en_US.utf8
    
    • 1
    • 2

    HOME

    当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。

    getenv 函数

    获取环境变量值

    char *getenv(const char *name);

    成功:返回环境变量的值;失败:NULL (name 不存在)

    setenv 函数

    设置环境变量的值

    int setenv(const char *name, const char *value, int overwrite);

    成功:0;失败:-1

    参数overwrite取值:

    1:覆盖原环境变量

    0:不覆盖。(该参数常用于设置新环境变量,如:ABC = haha-day-night)

    unsetenv 函数

    删除环境变量 name 的定义

    intunsetenv(constchar*name);

    成功:0;失败:-1
    注意事项:name 不存在仍返回 0(成功),当 name 命名为"ABC="时则会出错。

    二、进程控制

    2.1、fork

    1、用法

    创建一个子进程。pid_t fork(void);

    失败返回:-1;

    成功返回:;

    • 父进程返回子进程的 ID(非负)
    • 子进程返回 0

    pid_t 类型表示进程 ID,但为了表示-1,它是有符号整型。(0 不是有效进程 ID,init 最小,为 1)

    注意返回值,不是 fork 函数能返回两个值,而是 fork 后,fork 函数变为两个,父子需【各自】返回一个。

    也可以man page查询fork用法

    man 2 fork
    
    • 1
    image-20220915140944146

    RETURN VALUE说明:

    成功,会在父进程返回子进程的PID,同时在子进程返回0. 失败,会在父进程返回-1,同时子进程创建失败,并且设置errno

    2、用法示例

    fork.c

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
        printf("before fork-1-\n");     // 在fork之前打印,父进程执行,只执行一次
        printf("before fork-2-\n");
        printf("before fork-3-\n");
        printf("before fork-4-\n");
    
        pid_t pid = fork();             // 创建子进程
        if (pid == -1) {
            perror("fork error");
            exit(1);
    
        } else if (pid == 0) {          // 子进程
            
            printf("---child is created\n");
    
        } else if (pid > 0) {           // 父进程
    
            printf("---parent process: my child is %d\n", pid);
        }
        
        printf("===================end of file\n");  // 父子进程各自执行一次.
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    运行结果

    fork后的子进程与父进程会走同一段剩余代码,那么如何判断子进程与父进程?

    pid为0是子进程,pid大于0是父进程

    before fork-1-
    before fork-2-
    before fork-3-
    before fork-4-
    ---parent process: my child is 1504886
    ===================end of file
    ---child is created
    ===================end of file
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3、循环创建 n 个子进程

    一次 fork 函数调用可以创建一个子进程。那么创建 N 个子进程应该怎样实现呢?

    简单想,for(i = 0; i < n; i++) { fork() } 即可。但这样创建的是 N 个子进程吗?

    循环创建 n 个子进程
    循环创建 N 个子进程

    从上图我们可以很清晰的看到,当 n 为 3 时候,循环创建了(2^n)-1 个子进程,而不是 N 的子进程。需要在循环的过程,保证子进程不再执行 fork ,因此当(fork() == 0)时,子进程应该立即 break;才正确。

    loop_fork.c 通过命令行参数指定创建进程的个数,每个进程休眠 1S 打印自己是第几个被创 建的进程。如:第 1 个子进程休眠 0 秒打印:“我是第 1 个子进程”;第 2 个进程休眠 1 秒打 印:“我是第 2 个子进程”;第 3 个进程休眠 2 秒打印:“我是第 3 个子进程”。

    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
        int i;
        pid_t pid;
    
        for (i = 0; i < 5; i++) {
            if (fork() == 0)        // 循环期间, 子进程不 fork
                break;
        }
        if (5 == i) {       // 父进程, 从 表达式 2 跳出
            sleep(5);
            printf("I'm parent \n");
        } else {            // 子进程, 从 break 跳出
            sleep(i);
            printf("I'm %dth child\n", i+1);
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这里判断如果是子进程fork() == 0,则不fork

    运行结果

    I'm 1th child
    I'm 2th child
    I'm 3th child
    I'm 4th child
    I'm 5th child
    I'm parent 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.2、getpid && getppid(获取进程id)

    1、getpid

    获取当前进程 ID

    pid_t getpid(void);

    #include 
    #include 
    
    int main(int argc, char *argv[]) {
        pid_t pid = getpid();
        printf("getpid:%d\n", pid);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    打印

    getpid:65447

    2、getppid

    获取当前进程的父进程 ID

    pid_t getppid(void);

    区分一个函数是“系统函数”还是“库函数”依据:

    • 是否访问内核数据结构
    • 是否访问外部硬件资源

    二者有任一 > 系统函数;

    二者均无 > 库函数

    #include 
    #include 
    
    int main(int argc, char *argv[]) {
        pid_t pid = getppid();
        printf("getppid:%d\n", pid);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    打印

    getppid:63935

    2.3、getuid && getgid(获取进程用户id)

    1、getuid

    获取当前进程实际用户 ID

    uid_t getuid(void);

    获取当前进程有效用户 ID

    uid_t geteuid(void);

    实际用户ID与有效用户ID区别:

    大多数情况下等同,但是当设置-用户-ID(SUID)位设置,则有效用户ID等于文件的所有者的uid,而不是实际用户ID;同样,如果设置了设置-用户组-ID(SGID)位,则有效用户组ID等于文件所有者的gid,而不是实际用户组ID

    #include 
    #include 
    
    int main(int argc, char *argv[]) {
        uid_t uid = getuid();
        printf("uid_t:%d\n", uid);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    打印

    uid_t:501

    2、getgid

    获取当前进程使用用户组 ID

    gid_t getgid(void);

    获取当前进程有效用户组 ID

    gid_t getegid(void);

    #include 
    #include 
    
    int main(int argc, char *argv[]) {
        gid_t gid = getgid();
        printf("getgid:%d\n", gid);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    打印

    getgid:20

  • 相关阅读:
    熵 | 无线通信知识
    ManageEngine第六次在Gartner的SIEM魔力象限中获认可
    异地容灾系统和数据仓库中数据同步的设计软件的功能模型
    聊聊Maven的依赖传递、依赖管理、依赖作用域
    【剑指Offer】16.数值的整数次方
    docker- harbor私有仓库部署与管理
    CYEZ 模拟赛 2
    网络安全领域必备工具,其中一个绝了
    树的基本操作(数据结构)
    为什么要替换 Object.defineProperty?
  • 原文地址:https://blog.csdn.net/liuxingyuzaixian/article/details/126871476