1.前置知识补充

1.1计算机组成的5大部件

(1)运算器(cpu):也叫算数逻辑单元,完成对数据的各种常规运算,如加减乘除,也包括逻辑运算,移位,比较等。
(2)控制器:它是整个计算机系统的控制中心,它指挥计算机各部分协调地工作,保证计算机按照预先规定的目标和步骤有条不紊地进行操作及处理。
(3)存储器:存储程序和各种数据。
(4)输入设备:把人所熟悉的信息如,图片,声音,文字,转换为计算机能够识别的信息存储起来。
(5)输出设备:把计算机机能识别的信息转换为人能识别的信息,进行呈现。一台计算机可以抽象成下图: CPU 中包含控制器和运算器,内存就是存储器。I/O 设备就是输入设备和输出设备,如:键盘、显示器、鼠标、硬盘、网卡。

这里我们对内存空间和硬盘空间做一下区分,如下图所示:

只要记住一点,计算机掉电后就消失的就是存储在内存上的。掉电后依旧存在是存储在硬盘的。

1.2进程概论

(1)进程:一个正在运行的程序。

(2)PCB:即使进程的控制块,也是进程存在的唯一标志。用来描述进程的属性信息。

1.3内存管理

简单分页 逻辑页 物理页 页表

2.mian()主函数参数

我们先一个程序用于演示命令行参数(argcargv)和环境变量(envp)的基本使用。来验证主函数在运行时可以传入参数。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

// 参数个数 参数内容 环境变量
int main(int argc, char* argv[], char* envp[])
{
    printf("argc=%d\n", argc);

    for(int i = 0; i < argc; i++)
    {
        printf("argv[%d]=%s\n", i, argv[i]);
    }

    exit(0);
}

环境变量是从父进程中继承过来的。

总结一下,什么时候可以将缓冲区的内容通过内核打印到屏幕上

1、缓冲区放满了

2、强制刷新

3、程序结束

3.folk复制进程

首先我们要了解操作系统是如何管理进程的?

我们会先创建一个结构体(PCB进程控制块),结构体会描述这个进程的名字、类型、空间、在哪个地方存放等信息。操作系统用一个双向链表串起来了,有几个结点就有几个进程,如下图所示,都是这三个进程都由这个task_struct结构体进行串联的。

如果我现在要产生新进程呢,使用fork(),执行fork之后,复制当前进程(2238进程)当然ID要改变。

fork的返回值:pid_t fork();无参有返回值 int类型。规定 父进程的返回值是子进程的ID号(上图2239),而子进程的返回值是0;

我们实现一个fork进行演示:

一个程序可以通过 fork() 一分为二,让父子进程“各干各的”,同时运行。
父进程说“我是爸爸”,子进程说“我是孩子”,他们轮流输出自己的身份。 

/*
 * fork_demo.c
 * 演示:使用 fork() 创建子进程,并让父/子进程各自循环打印不同的字符串。
 * 要点:
 *   1) fork() 调用后会产生两个几乎相同的进程:父进程与子进程。
 *   2) 通过 fork() 的返回值区分当前代码是在父进程还是子进程中运行。
 *   3) 父/子进程分别设置各自的 n 和 s,然后各自循环打印,输出会交错。
 */

#include <stdio.h>      // printf
#include <stdlib.h>     // exit, EXIT_SUCCESS/EXIT_FAILURE
#include <unistd.h>     // fork, sleep
#include <sys/types.h>  // pid_t(不是必须,但包含更清晰)

// 形参:argc 参数个数;argv 参数数组;envp 环境变量指针数组(这里未使用)
int main(int argc, char* argv[], char* envp[])
{
    // s 用来指向要打印的字符串;n 表示循环打印的次数
    char* s = NULL;
    int n = 0;

    // ====== 关键:创建子进程 ======
    // fork() 成功会返回两次:
    //   在“子进程”里返回 0;
    //   在“父进程”里返回子进程的 PID(一个正数);
    //   失败时返回 -1。
    pid_t pid = fork();

    // 创建失败的情况,直接退出并返回错误码
    if (pid == -1)
    {
        // 也可以加 perror("fork"); 打印错误原因
        exit(EXIT_FAILURE);
    }

    // ====== 根据返回值区分父子进程 ======
    if (pid == 0)
    {
        // 这里是“子进程”的执行路径
        n = 3;            // 子进程循环打印 3 次
        s = "child";      // 子进程打印的内容
    }
    else
    {
        // 这里是“父进程”的执行路径
        n = 7;            // 父进程循环打印 7 次
        s = "parent";     // 父进程打印的内容
    }

    // ====== 循环打印并暂停 1 秒 ======
    // 两个进程会并发执行这段代码,因此输出顺序是不确定的,会交错出现。
    for (int i = 0; i < n; i++)
    {
        printf("s=%s\n", s);  // 打印当前进程的标识字符串
        sleep(1);             // 休眠 1 秒,方便观察交错输出
    }

    // ====== 正常退出进程 ======
    // exit(0) 会进行标准 I/O 刷新(把缓冲区内容写出)。
    // 如果你在子进程里不想再次刷新 I/O(例如重定向到文件时避免重复写出),
    // 可以换成 _exit(0)(需要 #include <unistd.h>),它不会做缓冲区刷新。
    exit(EXIT_SUCCESS);
}

父进程的n和子进程的n 不在同一块物理内存上。地址分为逻辑地址和物理地址,图中展示的就是逻辑地址,而物理地址如下图所示。我们一切打印出来的都是逻辑地址。

4.僵死进程

每一个运行中的进程有这样几种状态,就绪、运行、阻塞 这三种状态。还有一种特殊的僵死状态。

什么是僵死进程:子进程先结束 父进程没有收到子进程的退出码,该进程为僵死进程。(子进程先结束)。这个僵死进程持续到父进程使用wait()拿到子进程的退出码,这就算进程结束。

父进程没有wait子进程但是父进程结束了,子进程变成了孤儿进程,系统会为这个子进程重新分配一个父进程,这个父进程一定会收到子进程的退出码。

5.malloc

程序的内存布局结构(典型的 32 位进程地址空间布局)

  1. 内核空间(0xffff ffff 附近):进程地址空间的高地址部分,属于操作系统内核,用户进程无法直接访问,用于内核代码、数据、硬件管理等。

  2. 栈(Stack):从高地址向低地址增长,用于存储函数调用的上下文(如局部变量、返回地址),由系统自动分配 / 释放。

  3. 堆(Heap):从低地址向高地址增长,用于动态内存分配(如malloc/new申请的内存),由程序员手动管理。

  4. 数据段:存储全局变量、静态变量等(包括已初始化和未初始化的部分)。

  5. 代码段:存储程序的指令(可执行代码),通常是只读的,位于低地址(0x0000 0000 附近)。

看下面这段代码 通过malloc(1024*1024*1024)申请1GB(1024MB)的堆内存,把内存地址存到指针s中。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char *s = (char *)malloc(1024 * 1024 * 1024);
    if (s == NULL) {
        printf("malloc err\n");
        exit(1);
    }
    memset(s, 0, 1024 * 1024 * 1024);
    exit(0);
}

运行的时候内存一下子就满了

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐