Linux操作系统实验十一 进程管理(下)

简介: Linux操作系统实验十一 进程管理(下)

任务描述

fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程。那么,进程中的变量父进程和子进程是否都能使用并修改呢?

围绕问题的提出,我们尝试在父子进程中都修改同一个文件中的内容,最终将文件内容输出,便可知晓答案。

下面我们通过学习相关知识并编写代码来测试你的猜想是否正确。

相关知识

Linux 进程中的几个状态:

  • R 运行状态 (runing),并不意味着进程一定在运行中,也可以在运行队列里
  • S 睡眠状态 (sleeping),进程在等待事件完成(浅度睡眠,可以被唤醒)
  • D 磁盘睡眠状态 (Disk sleep),不可中断睡眠(深度睡眠,不可以被唤醒,通常在磁盘写入时发生)
  • T 停止状态 (stopped),可以通过发送 SIGSTOP 信号给进程来停止进程,可以发送 SIGCONT 信号让进程继续运行
  • Z 僵尸状态 (zombie),子进程退出,父进程还在运行,但是父进程没有读到子进程的退出状态,子进程进入僵尸状态

为了完成本关任务,你需要掌握:1. 如何创建进程;2.fork()函数的执行步骤。

进程的创建

在使用创建进程函数前,我们需要先导入unistd.h库。

创建进程的函数原型是:pid_t fork(void);

例如:

  1. pid_t pid = fork();

pid_t是一个整数类型,即fork()函数会返回新进程的 ID 号(0~32768的整数)。fork函数在父进程中返回子进程的pid,在子进程中返回0

注意在子进程中返回的0,并不是子进程的pid,子进程的pid在父进程的返回值中保存。而子进程的返回值是为了标识它是子进程,用来区分父子进程的。

父子进程的注意事项:

  1. 新进程是当前进程的子进程。
  2. 父进程和子进程 ①父进程:fork()的调用者; ②子进程:新建的进程。
  3. 子进程是父进程的复制(相同的代码,相同的数据,相同的堆栈),除了 ID 号和时间信息外,两者完全相同。
  4. 子进程和父进程可以并发运行。
fork()函数的执行步骤

由于子进程是父进程的复制,所以子进程中也会有创建子进程的语句,如果不加以限制,就会形成递归创建,但实际上并不是这样的。

实际流程是:父进程创建了子进程后,子进程中“创建进程”语句不再执行,并发运行其他语句。

在 Linux 的源码中我们可以找到fork函数:

  1. ...
  2. copy_files(clone_flags,p);    //克隆文件
  3. copy_fs(clone_flags,p);       //克隆文件系统
  4. copy_mm(clone_flags,p);       //克隆内存信息
  5. ...

我们可以看到有三条语句,用于拷贝进程的所有信息,这也解释了为什么说子进程是父进程的复制。

编程要求

通过提示,在右侧编辑器中补充代码,完成在指定文件中添加内容,具体要求如下:

  1. 创建进程;
  2. 父进程向文件中添加helloworld
  3. 子进程向文件中添加hellowelcome
  4. 只需修改文件内容即可,平台将为你输出文件内容。

提示:fork()函数的返回值为0时则为子进程。

测试说明

平台会对你编写的代码进行测试:

预期输出: hello world!hello welcome!

任务描述

Linux 中,init进程(初始化进程)是所有其他进程的父进程,那么是不是说就所有的进程都执行与init进程相同的功能了呢?

答案当然不是,Linux 中某些子进程和父进程的执行并不是完全相同的。他们是如何做到的呢?

下面我们就一起来学习进程的加载。

相关知识

为了完成本关任务,你需要掌握如何加载非父进程的进程。

exec函数族

Linux 中exec函数族,它是若干函数的集合。exec 函数族的作用是根据指定的文件名或目录名找到可执行文件,并用它来取代调用进程的内容。换句话说,其功能是让子进程具有和父进程完全不同的新功能。

exec本身并非一个函数,是指一组函数,一共有6种在进程中启动另一个程序的方法:

  1. int execl(const char *path, const char *arg, ...);
  2. int execv(const char *path, char *const argv[]);
  3. int execle(const char *path, const char *arg, ..., char * const envp[]);
  4. int execve(const char *path, char *const argv[], char *const envp[]);
  5. int execlp(const char *file, const char *arg, ...);
  6. int execvp(const char *file, char *const argv[]);

exec函数族的6个函数看起来十分复杂,实际上无论是作用还是用法都十分相似,他们的命名规则:

  • lv表示参数是以列表还是以数组的方式提供,且都必须以NULL结尾;
  • **p**代表在path环境变量中搜索file文件;
  • e表示该函数取envp[]数组,而不使用当前环境,即为程序提供新环境变量,一般很少使用。

进程调用一种 exec 函数时,该进程完全由新程序替换,而新程序则从其 main 函数开始执行。exec只是用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段。并没有创建新进程,所以进程的 ID 是不变的。

注意: 一旦exec函数执行成功,它就不会返回了,进程结束。但是如果exec函数执行失败, 它会返回失败的信息,并且进程继续执行后面的代码!执行失败的话,必须用exit()函数来让子进程退出!(exit函数调用需导入stdlib.h库函数)

进程加载

我们使用execl()函数来做示例:

  1. int execl(const char *path, const char *arg, ...)

函数参数说明: path:要执行的程序路径。可以是绝对路径或者是相对路径。在execvexecveexeclexecle4个函数中,使用带路径名的文件名作为参数。 arg:程序的第0个参数,即程序名自身。相当于argv[O]:命令行参数列表。调用相应程序时有多少命令行参数,就需要有多少个输入参数项。最后应该以NULL结尾,表明命令行参数结束。

返回值:-1表明调用exec失败,无返回表明调用成功。

  1. #include <unistd.h>
  2. int main()
  3. {
  4.    printf("before exec\n");
  5.    execl("/bin/ls", "ls", "-a", "-l", "-h", NULL);
  6.    //若 execl() 执行成功,下面则不执行,因为当前进程已经被执行的 ls 替换了
  7.    printf("after exec\n");
  8.    return 0;
  9. }

执行语句说明: /bin/ls:外部程序,这里是/bin目录的ls可执行程序,必须带上路径(相对或绝对) ls:没有意义,如果需要给这个外部程序传参,这里必须要写上字符串,至于字符串内容任意 -a,-l,-h:给外部程序ls传的参数 NULL:代表给外部程序 ls 传参结束

执行结果:

编程要求

在右侧编辑器补充代码,要求如下:

  1. 创建进程;
  2. 在父进程中输出entering main process---
  3. 在子进程中使用execl()函数调用src/step2/hello.c文件,测试环境中已将path置为src/step2,因此带路径的文件名为./hello

测试说明

平台会对你编写的代码进行测试:

预期输出: entering main process---Hello exec! This is another task。

任务描述

学习完进程的创建和加载,我们发现系统都是先执行父进程的内容再执行子进程,那么有什么方法可以使子进程先执行,父进程后执行的吗?

通过学习相关知识,我们需要编写一个先执行子进程内容后执行父进程内容的程序。

相关知识

为了完成本关任务,你需要掌握:1. 系统进程退出方法;2.系统进程等待方法。

进程退出

进程常见退出方式:

1.正常退出:从main()函数中返回return退出;调用exit()函数退出;调用_exit()函数退出。

2.异常退出:由信号终止;调用abort函数。

  • return:是常见的退出进程方式,main函数中执行return等同于执行exit函数,main函数中return n函数返回值作为exit(n)函数的参数。
  • exit():进程结束执行时调用,完成进程资源回收。函数原型:void exit(int status); 包含在stdlib.h库中,参数status为进程的终止状态,父进程可以通过wait()获取。exit(0)表示正常退出,exit(n)其中n不为0都表示异常退出。
  • _exit():正常退出的方式最终都会调用_exit()函数。函数原形:void _eixt(int status); 包含在unistd.h库中,参数同exit函数。
进程等待

创建子进程后如果父进程不等待,子进程退出后就会变成僵尸进程,直到父进程来获取退出信息才会释放剩余资源,并且此时该进程无法被信号杀死,继续占用资源造成内存泄露。因此,我们需要父进程调用等待函数来避免出现僵尸进程,进程等待函数是为了配合子进程的exit(),进而实现释放子进程资源。

进程等待方式,使用前需导入sys/wait.h库:

  • wait() 是一个阻塞式等待,必须等到有一个子进程退出后获取退出状态,释放资源才可以返回。
  1. pid_t wait(int * status);

返回值: 退出的子进程的pid,失败返回-1

参数: 输出型参数,用于获取子进程退出状态码,不关心可以置为NULL

  • waitpid() 是一个指定pid的等待方式。
  1. pid_t waitpid(pid_t pid, int * status, int options);

返回值: 返回退出进程的pid,当调用失败(没有子进程)返回-1。可以通过perror函数进行打印错误。

参数:pidpid=-1表示等待任一进程;pid=nn为指定需要等待的子进程,若n<0则等待和其绝对值的相同的子进程;pid=0表示等待和进程组pid相同的进程。

status:同wait函数参数status相同。

options:选项参数,options=0表示和wait()一样的阻塞等待;options=WNOHANG表示不阻塞,如果没有退出的进程或者需要等待的子进程将直接返回0,另外这个参数还可以设置成其它属性。

编程要求

根据提示,在右侧编辑器补充代码,创建两个子进程,第一个进程打印I am first process!,第二个进程打印I am second process!,父进程打印I am father process!

要求实现先打印第一个进程内容,再打印第二个进程内容,最后打印父进程内容。

提示:进程加载execl函数调用输出用法execl("/bin/echo","echo", "输出语句", NULL);

测试说明

平台会对你编写的代码进行测试:

预期输出: I am first process!I am second process!I am father process!

相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
目录
相关文章
|
6天前
|
算法 调度 UED
探索操作系统核心:进程管理与调度
【9月更文挑战第28天】在数字世界的心脏跳动着无数进程,它们像是细胞一样构成了操作系统的生命体。本文将深入探讨操作系统中进程管理与调度的奥秘,揭示如何通过精心设计的数据结构和算法来维护系统的稳定性和效率。我们将从进程的基本概念出发,逐步解析进程状态转换、进程同步机制,以及进程调度策略,旨在为读者呈现一幅清晰、生动的操作系统内部工作机制图景。
|
10天前
|
算法 调度 UED
探索操作系统中的进程调度:理论与实践
【9月更文挑战第24天】 在数字世界的心脏跳动着的是操作系统,它像一位精明的指挥家,精心安排每个音符的演奏。本文将带你进入操作系统的内核,一探进程调度的秘密。我们将从简单的批处理系统谈起,穿越时间隧道,见证现代多道程序设计系统的复杂性与优雅。你将看到代码如何赋予理论以生命,理解调度算法背后的哲理。让我们一起跟随甘地的指引,成为我们希望在世界上看到的改变。
|
2天前
|
算法 调度 UED
探索操作系统的心脏:进程调度算法
【9月更文挑战第32天】在数字世界的每一次心跳中,都隐藏着一个不为人知的英雄——进程调度算法。它默默地在后台运作,确保我们的命令得到快速响应,应用程序平稳运行。本文将带你走进操作系统的核心,一探进程调度的奥秘,并通过代码示例揭示其背后的智慧。准备好跟随我一起深入这趟技术之旅了吗?让我们开始吧!
|
4天前
|
算法 Linux 调度
深入理解操作系统的进程调度
【9月更文挑战第30天】本文将带你进入操作系统的核心—进程调度。我们将探讨其工作原理,分析几种常见的调度算法,并通过实际代码示例来揭示这些理论是如何在真实系统中实现的。无论你是初学者还是有经验的开发者,这篇文章都能帮助你更好地理解操作系统的这一关键组成部分。
|
4天前
|
消息中间件 算法 调度
探索操作系统核心:进程管理与调度策略
【9月更文挑战第30天】在数字化时代的心脏,操作系统扮演着至关重要的角色。本文将深入探讨操作系统的基石之一——进程管理,以及如何通过调度策略优化系统性能。我们将从进程的基本概念出发,逐步解析进程状态、进程控制和进程间通信等关键要素。同时,我们会探讨几种常见的进程调度算法,并分析它们的优缺点。最后,文章将展示一个简单的代码示例,以加深对理论部分的理解和应用。
|
5天前
|
算法 调度 UED
探索操作系统的心脏:进程管理与调度
【9月更文挑战第29天】在数字世界的海洋中,操作系统是支撑软件与硬件和谐共舞的桥梁。本文将深入探讨操作系统的核心功能—进程管理及其调度机制,揭示它们是如何影响计算机性能和用户体验的。通过浅显易懂的语言和生动的比喻,我们将一起遨游在进程的生命周期、调度算法以及优先级等概念之间,旨在为读者呈现一个清晰的操作系统内部运作图景。
17 6
|
4天前
|
算法 调度 开发者
深入理解操作系统之进程管理与调度
【9月更文挑战第30天】本文旨在通过浅显易懂的语言和具体代码示例,带领读者探索操作系统中进程管理的奥秘。我们将从进程的生命周期出发,逐步解析进程调度的核心概念,并通过实例展示如何实现简单的进程调度算法。无论你是初学者还是有一定基础的开发者,都能在这篇文章中找到有价值的信息,帮助你更好地理解和掌握进程管理与调度的知识。
14 4
|
6天前
|
算法 调度
操作系统的心脏:深入解析进程调度算法
本文旨在深入探讨现代操作系统中的核心功能之一——进程调度。进程调度算法是操作系统用于分配CPU时间片给各个进程的机制,以确保系统资源的高效利用和公平分配。本文将详细介绍几种主要的进程调度算法,包括先来先服务(FCFS)、短作业优先(SJF)、时间片轮转(RR)以及优先级调度(PS)。我们将分析每种算法的基本原理、优缺点及其适用场景。同时,本文还将讨论多级反馈队列(MFQ)调度算法,并探讨这些算法在实际应用中的表现及未来发展趋势。通过深入解析这些内容,希望能够为读者提供对操作系统进程调度机制的全面理解。
|
7天前
|
算法 调度 UED
探索操作系统中的进程调度
【9月更文挑战第27天】操作系统是计算机的灵魂,而进程调度则是其跳动的心脏。本文将深入浅出地探讨进程调度机制,从理论到实践,带你领略这一技术的魅力和复杂性。我们将通过代码示例,揭示调度算法如何影响系统性能和用户体验。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇理解操作系统深层工作原理的大门。
17 6
|
9天前
|
存储 算法 前端开发
深入理解操作系统:进程调度与优先级队列算法
【9月更文挑战第25天】在操作系统的复杂世界中,进程调度是维持系统稳定运行的核心机制之一。本文将深入探讨进程调度的基本概念,分析不同的进程调度算法,并着重介绍优先级队列算法的原理和实现。通过简洁明了的语言,我们将一起探索如何优化进程调度,提高操作系统的效率和响应速度。无论你是计算机科学的初学者还是希望深化理解的专业人士,这篇文章都将为你提供有价值的见解。
下一篇
无影云桌面