Linux系统中程序的执行过程

Linux是一种免费、开源的操作系统内核,它是一个类Unix的操作系统,可以在各种计算机硬件平台上运行。Linux内核是Linux操作系统的核心部分,它管理系统的硬件资源,并为用户空间提供服务。

Linux以其稳定性、安全性、灵活性和可定制性而闻名。它被广泛用于服务器、嵌入式系统、超级计算机以及个人电脑等各种领域。此外,许多云计算平台和移动设备也采用了Linux操作系统或基于Linux的系统。

1. 进程的创建

在Linux系统中,进程的创建通常通过调用系统调用fork()clone()来实现。这两个系统调用都可以用来创建新的进程,但clone()提供了更灵活的选项,允许创建的新进程共享某些资源。

1.fork()系统调用

这是创建新进程最常用的方法之一。当一个进程调用fork()时,它创建了一个与自身几乎完全相同的子进程。子进程继承了父进程的地址空间、文件描述符和其他资源。fork()系统调用的语法如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid;

    // 使用fork()创建一个新进程
    pid = fork();

    // 检查fork()的返回值,以确定当前是父进程还是子进程
    if (pid < 0) {
        // 错误处理
        fprintf(stderr, "Fork failed\n");
        return 1;
    } else if (pid == 0) {
        // 子进程代码
        printf("Child process, PID: %d\n", getpid());
    } else {
        // 父进程代码
        printf("Parent process, PID: %d, Child PID: %d\n", getpid(), pid);
    }

    // 这段代码会被父进程和子进程同时执行
    printf("This line is printed by both parent and child processes\n");

    return 0;
}

这段代码中,首先使用fork()创建一个新的进程。然后根据fork()的返回值来区分父进程和子进程:如果fork()返回负值,则表示创建进程失败;如果返回值为0,则表示当前进程是子进程;如果返回值大于0,则表示当前进程是父进程,返回值为子进程的PID。

根据这个区分,我们在父进程和子进程分别输出不同的消息。最后,父进程和子进程都会执行最后一行代码,输出相同的消息。

2.clone()系统调用

clone()系统调用比fork()更灵活,它允许创建的新进程共享某些资源,例如内存空间、文件描述符表等。这使得clone()可以用于创建线程,因为线程之间通常需要共享内存空间。clone()系统调用的语法如下:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <sys/types.h>
#include <unistd.h>

#define STACK_SIZE 1024*1024 // 定义新进程栈的大小

// 新进程的入口函数
int child_function(void *arg) {
    printf("Child process, PID: %d\n", getpid());
    return 0;
}

int main() {
    char *stack; // 新进程的栈空间
    pid_t pid;

    // 分配新进程的栈空间
    stack = malloc(STACK_SIZE);
    if (stack == NULL) {
        fprintf(stderr, "Failed to allocate memory for stack\n");
        exit(EXIT_FAILURE);
    }

    // 使用clone()创建新进程,传递新进程的入口函数和栈空间
    pid = clone(child_function, stack + STACK_SIZE, CLONE_VM | CLONE_THREAD | CLONE_SIGHAND | CLONE_FS | CLONE_FILES, NULL);
    if (pid == -1) {
        fprintf(stderr, "Clone failed\n");
        exit(EXIT_FAILURE);
    }

    // 等待子进程结束
    if (waitpid(pid, NULL, 0) == -1) {
        fprintf(stderr, "Waitpid failed\n");
        exit(EXIT_FAILURE);
    }

    printf("Parent process, PID: %d, Child PID: %d\n", getpid(), pid);

    // 释放栈空间内存
    free(stack);

    return 0;
}

在这个示例中,首先定义了一个新进程的入口函数child_function,该函数在子进程中执行。然后,使用malloc()分配了新进程的栈空间。接下来,使用clone()系统调用创建了一个新的进程,传递了新进程的入口函数和栈空间。在clone()的第三个参数中,使用了一系列标志来指定新进程与父进程之间共享的资源。最后,父进程等待子进程结束,并输出相应的信息。

需要注意的是,clone()系统调用的使用较为复杂,需要谨慎处理共享资源和线程安全性等问题。

2. 可执行程序的加载

在Linux中,可执行程序的加载和执行流程可以通过一个简单的C程序来说明。以下是一个示例程序,它演示了如何使用Linux系统调用execve()来加载并执行一个可执行文件:

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

int main() {
    char *args[] = {"/bin/ls", "-l", NULL}; // 可执行文件路径及参数列表

    // 调用execve()系统调用加载并执行/bin/ls
    if (execve("/bin/ls", args, NULL) == -1) {
        perror("execve"); // 输出错误信息
        exit(EXIT_FAILURE);
    }

    // 如果execve()调用成功,则不会执行到这里
    printf("This line will not be executed\n");
    return 0;
}

这个示例程序的功能是执行/bin/ls -l命令。在execve()系统调用中,第一个参数是要执行的可执行文件的路径,第二个参数是一个字符串数组,包含了命令的参数列表,最后一个参数通常为NULL,表示环境变量使用当前环境。如果execve()调用成功,当前进程将被替换为新的进程,而不会继续执行原来的代码。

要编译并运行这个程序,可以按照以下步骤:

这就是大致的Linux中可执行程序的加载流程。加载器在这个过程中扮演了关键的角色,负责将可执行文件加载到内存中,并进行必要的链接和重定位操作。

  • 将上述代码保存到一个文件(例如exec_example.c)中。
  • 使用编译器编译该程序:
  • gcc -o exec_example exec_example.c
    

    运行编译后的可执行文件:

  • ./exec_example
    

    程序将加载/bin/ls命令并执行,并输出相应的目录信息。

    这个示例展示了在Linux中加载和执行一个可执行文件的基本流程,通过execve()系统调用,当前进程被替换为新的进程。

  • 在Linux中,可执行程序的加载流程可以概括为以下几个主要步骤:

  • 加载器的启动: 当你执行一个可执行文件时,操作系统会创建一个新的进程。此时,加载器(通常是/lib64/ld-linux-x86-64.so.2)会被启动。

  • 解析ELF文件头: 加载器会读取可执行文件的ELF(Executable and Linkable Format)文件头,确定程序的类型(可执行文件、共享库等)、入口地址、段(段包括代码段、数据段等)的位置和大小等信息。

  • 分配内存空间: 加载器会为程序分配内存空间,包括代码段、数据段、堆和栈等。

  • 加载程序段: 加载器将可执行文件中的各个段加载到分配的内存空间中。这些段包括代码段(.text)、数据段(.data)、只读数据段(.rodata)、BSS段(未初始化的数据)、堆和栈等。

  • 重定位(可选): 如果程序依赖于共享库或其他可执行文件,则需要进行重定位操作,将程序中的符号引用重定位到正确的地址。这个过程包括符号解析和符号重定位。

  • 加载共享库(可选): 如果可执行文件依赖于共享库,加载器会加载这些共享库,并将它们映射到程序的地址空间中。

  • 动态链接(可选): 加载器会根据可执行文件中的动态链接信息,将共享库中的符号链接到程序中。这个过程包括符号解析和符号重定位。

  • 设置堆栈和程序入口: 加载器会设置程序的堆栈,以及程序的入口地址,然后将控制权转移到程序的入口地址处。

  • 程序执行: 一旦控制权转移到程序的入口地址,程序开始执行。它会按照指令序列执行,并在需要时调用系统调用或其他函数完成所需的操作。

三、进程调度

操作系统中的进程调度算法用于确定下一个要执行的进程,以及分配处理器时间片的方式。以下是一些常见的进程调度算法:

  1. 先来先服务(First Come First Served,FCFS): 这是最简单的调度算法之一。按照进程到达的顺序分配处理器时间片。进程按照到达的顺序排队,然后按照队列的顺序依次执行。这种算法易于实现,但可能会导致长作业等待时间过长,因为短作业可能被长作业阻塞。

  2. 短作业优先(Shortest Job First,SJF): 这种算法优先执行估计执行时间最短的进程。它可以减少平均等待时间,并且在没有新的进程到达时,是最优的非抢占式调度算法。然而,实际中很难准确估计每个进程的执行时间。

  3. 最短剩余时间优先(Shortest Remaining Time First,SRTF): 这是SJF的抢占式版本,当新的进程到达或者一个进程被阻塞时,系统会选择剩余执行时间最短的进程执行。它可以减少响应时间,但需要频繁的上下文切换,可能会增加系统开销。

  4. 轮转调度(Round Robin,RR): 这是一种基于时间片的调度算法,每个进程被分配一个小的时间片,当时间片用完时,进程被暂停,放入队列的尾部,等待下一次调度。它可以保证公平性,并且适用于时间共享系统,但可能会导致上下文切换过多。

  5. 优先级调度(Priority Scheduling): 这种算法为每个进程分配一个优先级,并且系统总是选择优先级最高的进程来执行。它可以根据进程的重要性和紧急程度来调度,但可能导致低优先级的进程饥饿。

  6. 多级反馈队列调度(Multilevel Feedback Queue,MLFQ): 这是一种结合了轮转调度和优先级调度的调度算法。系统维护多个就绪队列,每个队列具有不同的优先级,并且每个队列的时间片大小逐渐增加。进程首先被插入到最高优先级队列中,如果它在时间片内执行完毕,则从队列中移除;否则,进程被移到下一级队列,依次类推。这种算法既能保证响应时间,又能保证长作业的运行。

Linux中的进程调度过程是由内核负责的,主要分为以下几个步骤:

  1. 就绪队列管理: 内核维护一个就绪队列,其中包含所有准备好运行的进程。这些进程等待分配处理器时间片并开始执行。

  2. 选择下一个进程: 内核根据特定的调度策略从就绪队列中选择下一个要执行的进程。常见的调度策略包括完全公平调度(CFS)、实时调度(Real-Time Scheduling)等。

  3. 进程切换: 一旦选择了下一个要执行的进程,内核会进行进程切换。这包括保存当前正在执行进程的上下文(寄存器状态、内存映射等),然后恢复下一个要执行的进程的上下文。

  4. 执行进程: 一旦切换到下一个进程,内核会将控制权转移到该进程,并开始执行它的代码。进程将按照其指令序列执行,并在需要时调用系统调用或其他函数来完成所需的操作。

  5. 时间片管理: 内核会为每个进程分配一个时间片,用于执行其代码。一旦进程的时间片用尽,内核会将控制权转移到下一个进程,并将该进程移到就绪队列的尾部,等待下一次调度。

  6. 调度器调度间隔: 在调度器中还存在一个调度间隔,用于控制调度器的频率。在每个调度间隔结束时,内核会重新评估就绪队列中的进程,并选择下一个要执行的进程。

这些是Linux中进程调度的基本过程。在实际情况下,还可能涉及到更多的细节和优化,以满足系统的性能和响应需求。

  • 21
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值