Linux进程管理

Linux进程管理

进程的定义与特征

进程的定义

程序:是静态的,就是个存放在磁盘里的可执行文件,就是一系列的指令集合。

进程:是动态的,是程序的一次执行过程。

进程的组成--PCB

  • 一个程序文件program,只是一堆待执行的代码和部分待处理的数据,他们只有被加载到内存中,然后让CPU逐条执行其代码,根据代码做出相应的动作,才形成一个真正“活的”、动态的进程process,因此进程是一个动态变化的过程,而程序文件只是这一系列动作的原始蓝本,是一个静态的剧本。

当进程被创建时,操作系统会为该进程分配一个** 唯一的 ** , ** 不重复 ** 的“身份证号”--PID。

  • 进程标识符(process identifier)

用于区分系统中的其他进程,这个PID也是Linux内核提供给用户访问进程的一个接口,用户可以通过PID控制进程。

  • PCB --ProcessControl Block

ELF格式的程序被执行时,内核中实际上产生了一个叫task_struct{}的结构 体来表示这个进程。进程是一个“活动的实体”,这个活动的实体从一开始诞生就需要各种 各样的资源以便于生存下去,比如内存资源、CPU资源、文件、信号、各种锁资源等,所 有这些东西都是动态变化的,这些信息都被事无巨细地一一记录在结构体task_struct之 中,所以这个结构体也常常被成为进程控制块(PCB,即Process Control Block)。该结构体被定义在一个名称叫做sched.h的头文件中。

PCB是进程存在的唯一标志

image-20240525091741801

  • 进程是操作系统分配资源的基本单位!

  • 操作系统是以进程为单位来分配系统资源的,比如内存空间、CPU使用权等。

  • 线程是操作系统调度资源的最小单位!

  • 进程包含线程!

image-20240526135129498

  • PCB是给操作系统用的。
  • 程序段和数据段是给进程自己用的。

进程的特征

  1. 并发性:并发性指的是多个进程实体可以同时存在于内存中,并在一段时间内同时运行。这是进程的重要特征,也是操作系统的关键特性之一。例如,多个应用程序可以同时运行,共享计算机资源。
  2. 动态性:进程的实质是程序的一次执行过程。因此,进程是动态产生和动态消亡的。每个进程都有自己的生命周期,从创建到终止。
  3. 独立性:进程实体是一个独立运行、独立分配资源和独立接受调度的基本单位。每个进程都有自己的内存空间、寄存器和其他资源。这使得进程之间相互隔离,不会相互干扰。
  4. 异步性:进程按照各自独立的、不可预知的速度向前推进。这意味着不同进程之间的执行是异步的,它们不会严格按照某个固定的顺序运行。例如,一个进程可能在等待用户输入时暂停,而另一个进程继续执行

image-20240526140000676

Linux 系统查看进程pid的shell命令

ps -ef ps -aux 查看所有用户的相关进程的所有消息

进程的状态与转换

image-20240526114252952

  1. 新建态(New):进程刚刚被创建,但还没有被操作系统调度执行。在这个状态下,操作系统会为进程分配必要的资源,例如内存空间和标识符。
  2. 就绪态(Ready):进程已经准备好运行,等待被操作系统调度到CPU上执行。在这个状态下,进程已经被添加到可执行进程队列中,但还没有获得CPU的使用权。
  3. 运行态(Running):进程正在被CPU执行。它占有处理器,其程序正在运行。在单处理机系统中,只有一个进程处于执行状态;在多处理机系统中,可能有多个进程处于执行状态。
  4. 阻塞态(Blocked):进程因为某些原因(例如等待I/O操作完成、等待资源等)而暂时无法继续执行,被阻塞。在这个状态下,进程不会占用CPU资源,但会等待外部事件的发生。
    • 可中断等待状态:进程可以被信号量或中断唤醒,一旦资源有效,进程会立即进入就绪状态。
    • 不可中断等待状态:进程不能被信号量或中断唤醒,只有当它申请的资源有效时才能被唤醒。这种状态通常用于内核中某些场景,例如磁盘读写时的DMA操作。
  5. 终止态(Terminated):进程因某种原因而中止运行,占有的所有资源将被回收。
    • 系统对其不再予以理睬,也称为“僵死状态”。进程则成为僵尸进程。

进程控制

进程控制是对系统中所有进程从创建、执行到撤销的全过程实行有效的管理和控制。

进程控制一般是由操作系统内核的相应程序(原语)来实现。通常,操作系统内核运行在系统态。

  • 原语

原语是由若干条指令组成的,用于完成特定功能的,具有原子性(不可分割)的子程序。它与一般过程的区别:它们是原子操作(Action Operation)为保证操作的正确性,原语在执行期间是不可被中断的。因此,规定在执行原语操作时要屏蔽中断,以保证原语操作的不可分割性。

用于进程控制过程中的原语有:

  • 创建原语(Create)、撤销原语(Termination)
  • 阻塞原语(Block)、 唤醒原语(Wakeup)
  • 挂起原语(Suspend)、 激活原语(Active)

进程的创建

fork

函数名称 fork()
头文件 #include <unistd.h>
原型 pid_t fork(void);
返回值 - 成功:在父进程中返回大于0的正整数,表示子进程的PID。
- 成功:在子进程中返回0。
- 失败:返回-1,表示创建子进程失败。
备注 - fork() 执行成功后,会生成一个新的子进程。
- 在新的子进程中,fork() 返回值为0。
- 在原来的父进程中,fork() 返回值为大于0的正整数,即子进程的PID。
  1. fork() 函数的作用

    fork()会使得进程本身被复制,类似细胞分裂。因此,被创建出来的子进程和父进程几乎是一模一样的。但需要注意的是,子进程并不是100%父进程的复印件,具体关系如下:

    • 父子进程的以下属性在创建之初完全一样,子进程相当于搞了一份复制品:
      • 实际 UID 和 GID,以及有效 UID 和 GID。
      • 所有环境变量。
      • 进程组 ID 和会话 ID。
      • 当前工作路径(除非使用 chdir() 进行修改)。
      • 打开的文件。
      • 信号响应函数。
      • 整个内存空间,包括栈、堆、数据段、代码段、标准 I/O 的缓冲区等等。
    • 而以下属性,父子进程是不一样的:
      • 进程号 PID。PID 是身份证号码,哪怕亲如父子,也要区分开。
      • 记录锁。如果父进程对某文件加了锁,子进程不会继承这把锁。
      • 挂起的信号。这些信号是所谓的“悬而未决”的信号,等待着进程的响应,子进程也不会继承这些信号。
  2. 子进程的执行

    • 子进程会从 fork() 返回值后的下一条逻辑语句开始运行。这样就避免了不断调用 fork() 而产生无限子孙的悖论。
  3. 父子进程的关系

    • 父子进程是相互平等的:他们的执行次序是随机的,或者说他们是并发运行的,除非使用特殊机制来同步他们,否则你不能判断他们的运行究竟谁先谁后。
    • 父子进程是相互独立的:由于子进程完整地复制了父进程的内存空间,因此从内存空间的角度看,他们是相互独立、互不影响的。
    • 获取当前进程PID的函数接口getpid(),获取当前进程的父进程PID的函数接口是getppid()

    示例:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    int main(int argc, char const *argv[])
    {
    	int fd = 3; // 父进程的数据段
    
    	//创建子进程,调用fork函数的时候就已经创建子进程
    	pid_t child_pid = fork(); 
    
    	//通过fork函数的返回值分析父进程 or 子进程
    	if (child_pid > 0)
    	{
    		//说明是父进程的进程空间
    		printf("my is parent,my pid = %d,my child pid = %d\n",getpid(),child_pid);
    
    	}
    	else if( child_pid == 0)
    	{
    		//说明是子进程的进程空间
    		printf("my is child,my pid = %d,my parent pid = %d\n",getpid(),getppid());
    	}
    	else
    	{
    		printf("child process fork error\n");
    		return -1;
    	}
    
    	return 0;
    
    }
    

    image-20240526205547592

    由于父子进程的并发性,以上程序的执行效果是不一定的。

进程的撤销

wait

功能 等待子进程结束
头文件 #include <sys/wait.h>
原型 pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
参数 - pid
- 小于-1:等待组ID的绝对值为pid的进程组中的任一子进程。
- -1:等待任一子进程。
- 0:等待调用者所在进程组中的任一子进程。
- 大于0:等待进程组ID为pid的子进程。
- stat_loc:子进程退出状态。
- options
- WCONTINUED:报告任一从暂停态出来且从未报告过的子进程的状态。
- WNOHANG:非阻塞等待。
- WUNTRACED:报告任一当前处于暂停态且从未报告过的子进程的状态。
返回值 - wait()
- 成功:退出的子进程PID。
- 失败:-1。
- waitpid()
- 成功:状态发生改变的子进程PID(如果WNOHANG被设置,且由pid指定的进程存在但状态尚未发生改变,则返回0)。
- 失败:-1。
备注 如果不需要获取子进程的退出状态,stat_loc可以设置为NULL

所谓的退出状态不是退出值,退出状态包括了退出值。如果使用以上两个函数成功获取了子进程的退出状态,则可以使用以下宏来进一步解析

含义
WIFEXITED(status) 如果子进程正常退出,则该宏为真。
WEXITSTATUS(status) 如果子进程正常退出,则该宏将获取子进程的退出值。
WIFSIGNALED(status) 如果子进程被信号杀死,则该宏为真。
WTERMSIG(status) 如果子进程被信号杀死,则该宏将获取导致他死亡的信号值。
WCOREDUMP(status) 如果子进程被信号杀死且生成核心转储文件(coredump),则该宏为真。
WIFSTOPPED(status) 如果子进程的被信号暂停,且option中WUNTRACED已经被设置时,则该宏为真。
WSTOPSIG(status) 如果WIFSTOPPED(status)为真,则该宏将获取导致子进程暂停的信号值。
WIFCONTINUED(status) 如果子进程被信号SIGCONT重新置为就绪态,该宏为真。
status是一个出参,由操作系统为其赋值,用户可以传递NULL值表示不关心,而如果传入参数,操作系统就会根据该参数,将子进程的退出信息反馈给父进程,由status最终被赋予的值来体现。

image-20240526223251646

image-20240526223312076

可以看出,不论是正常退出还是异常退出,status的高8个比特位(只讨论低16个比特位)都表示子进程的退出码,而这个退出码一般是return的返回值或者exit的参数;正常退出时,status的低8个比特位为全0;而异常退出时,其第8个比特位则为core dump标志位,用来标志是否会有core dump文件产生,而低7个比特位则是退出信号。

可以通过位运算判断是否正常退出,是否产生core dump文件。

示例:

child_elf.c

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

int main(void)

{
    printf("[%d]:yep,I am the child\n", (int)getpid());
    exit(0);
}

wait.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/wait.h> // 包含 wait 函数的声明
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

int main(int argc, char** argv) {
    pid_t x = fork();

    if (x == 0) { // 子进程,执行指定程序 child_elf
        execl("./child_elf", "child_elf", NULL);
    }

    if (x > 0) { // 父进程,使用 wait() 阻塞等待子进程的退出
        int status;
        wait(&status);

        if (WIFEXITED(status)) { // 判断子进程是否正常退出
            printf("child exit normally, exit value: %hhu\n", WEXITSTATUS(status));
        }

        if (WIFSIGNALED(status)) { // 判断子进程是否被信号杀死
            printf("child killed by signal: %u\n", WTERMSIG(status));
        }
    }

    return 0;
}

父进程使用 wait() 阻塞等待子进程的退出状态。如果子进程正常退出,我们获取其退出值;如果子进程被信号杀死,我们获取导致其死亡的信号值。

image-20240526224752916

进程的执行

也可以叫程序的替换。

进程程序替换与fork不同,它并不会创建新的进程,而是该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。替换前后的进程号并未改变。

exec函数簇

功能 在进程中加载新的程序文件或者脚本,覆盖原有代码,重新运行
头文件 <unistd.h>
原型 int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
参数 - path:即将被加载执行的ELF文件或脚本的路径
- file:即将被加载执行的ELF文件或脚本的名字
- arg:以列表方式罗列的ELF文件或脚本的参数
- argv:以数组方式组织的ELF文件或脚本的参数
- envp:用户自定义的环境变量数组
返回值 成功不返回
失败返回 -1
备注 1. 函数名带字母 l 意味着其参数以列表(list)的方式提供。
2. 函数名带字母 v 意味着其参数以矢量(vector)数组的方式提供。
3. 函数名带字母 p 意味着会利用环境变量 PATH 来找寻指定的执行文件。
4. 函数名带字母 e 意味着用户提供自定义的环境变量。

system

功能 在子进程中执行一个 shell 命令,然后返回
头文件 <stdlib.h>
原型 int system(const char *command);
参数 - command:要执行的 shell 命令字符串。如果 commandNULL,则返回 shell 是否存在的状态
返回值 - 如果 commandNULL
- 返回 0:没有可用的 shell
- 返回 非 0 值:有可用的 shell
- 如果 command 不为 NULL
- 成功:返回 shell 的退出状态码
- 失败:返回 -1,并设置 errno
备注 1. system 函数使用 /bin/sh 来执行命令。
2. 在调用 system 之前,会忽略 SIGINTSIGQUIT 信号,并且阻塞 SIGCHLD 信号。
3. 如果在 system 调用期间捕获到信号,shell 的退出状态可能会受到影响。

示例:

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

int main(int argc, char const *argv[]) {
    // 创建子进程,调用fork函数的时候就已经创建子进程
    pid_t child_pid = fork();

    // 通过fork函数的返回值分析父进程或子进程
    if (child_pid > 0) {
        // 父进程的进程空间
        printf("my is parent, my pid = %d, my child pid = %d\n", getpid(), child_pid);
        wait(NULL); // 会阻塞,当第一个子进程状态改变时,该函数可以解除阻塞
    } else if (child_pid == 0) {
        // 子进程的进程空间
        printf("my is child, my pid = %d, my parent pid = %d\n", getpid(), getppid());
        
        // 打算让子进程加载新的代码段和数据段,也就是让子进程执行新的可执行文件
        //execl("./demo","demo",NULL);
        system("./demo"); // 使用system调用执行新的可执行文件 demo
        
    } else {
        // fork失败
        printf("child process fork error\n");
        return -1;
    }

    return 0;
}

进程的终止

_exit exit

功能 退出本进程
头文件 <unistd.h>
<stdlib.h>
原型 void _exit(int status);
void exit(int status);
参数 status:子进程的退出值
- 如果子进程正常退出,则 status 一般为0。
- 如果子进程异常退出,则 status 一般为非0。
返回值 不返回
备注 - exit() 函数退出时会自动冲洗(flush)标准IO缓冲区的残留数据到内核,如果进程注册了退出处理函数,还会自动执行这些函数。
- _exit() 函数直接退出,不执行任何清理操作。
- 除了\n(换行)以及exit()函数会刷新缓冲区之外,也可以调用fflush()来强制刷新缓冲区
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    int status;

    printf("父进程开始执行。\n");

    // 创建子进程
    pid_t child_pid = fork();

    if (child_pid == -1) {
        // 创建子进程失败
        perror("fork");
        return 1;
    } else if (child_pid == 0) {
        // 子进程
        printf("子进程正在执行,PID:%d\n", getpid());

        // 模拟一个错误条件
        int result = 5 / 0;  // 这会导致除以零异常

        // 检查除法是否成功
        if (result != 0) {
            // 如果发生错误,以状态码 1 退出
            printf("子进程遇到错误。\n");
            exit(1);
        } else {
            // 如果成功,以状态码 0 退出
            printf("子进程成功执行。\n");
            exit(0);
        }
    } else {
        // 父进程
        printf("父进程等待子进程结束,子进程PID:%d\n", child_pid);
        wait(&status);  // 等待子进程结束

        if (WIFEXITED(status)) {
            printf("子进程以状态码 %d 正常退出。\n", WEXITSTATUS(status));
        } else {
            printf("子进程未正常退出。\n");
        }

        printf("父进程结束。\n");
    }

    return 0;
}

image-20240526235400702

错误处理 (补充)

fprintf(stderr, "open user.txt fail, error = %d, %s\n", errno, strerror(errno));
  1. fprintf: 这个函数用于向流中写入格式化的输出。在这个例子中,它将格式化的消息写入标准错误流(stderr)。
  2. "open user.txt fail, error = %d, %s\n": 这是格式字符串。它包含了将要插入字符串的占位符。具体来说:
    • %d 是一个整数值的占位符(在这个例子中,是 errno 的值)。
    • %s 是一个字符串值的占位符(在这个例子中,是 strerror(errno) 的结果)。
  3. errno: 这个全局变量保存了最后一个失败的系统调用的错误代码。当发生错误时,系统调用和库函数会设置这个变量。
  4. strerror(errno): 这个函数接受一个错误号(如 errno),并返回一个人类可读的描述错误的字符串。它将错误代码转换为有意义的错误消息。

示例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char const *argv[]) {
    // 1. 打开文件
    int file_fd = open("./user.txt", O_RDWR | O_CREAT);
    if (file_fd == -1) {
        fprintf(stderr, "open user.txt fail, error = %d, %s\n", errno, strerror(errno));
        return -1;
    }

    // 2. 对文件进行写入
    write(file_fd, "hello world", 11);

    lseek(file_fd,0,SEEK_SET);  //将光标偏移到开头

    // 3. 从文本中读取数据
    char recv_buf[11] = {0};
    read(file_fd, recv_buf, 11);
    printf("read from user.txt data is [%s]\n", recv_buf);

    // 4. 关闭文件
    close(file_fd);

    return 0;
}

image-20240526231054451

posted @   sanjiudemiao  阅读(37)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示