当前位置:   article > 正文

Linux系统编程——Linux进程概念_系统调用如何识别进程id

系统调用如何识别进程id

Linux系统编程——Linux进程概念

1 操作系统(Operator System)

1.1 概念

操作系统是管理各种硬件资源,为程序应用提供基础,相当于计算机硬件和用户之间的中介。笼统的理解:操作系统包括了①内核(进程管理,内存管理,文件管理,驱动管理)②其他程序(函数库,shell程序等等)。

1.2 设计OS的目的
  • 计算机的硬件暴露出来的是一种**"丑陋"的接口**,操作系统可以管理硬件,并对用户暴露一些更加可靠的接口,方便用户使用。
  • OS可以合理地组织计算机的工作流程,加速程序的运行
1.3 系统调用
  • 在开发角度,操作系统为了使用户申请或释放资源,但为了不让用户直接操作硬件,给用户和硬件直接提供了一个系统调用,这是由操作系统提供的接口。
  • 库函数是语言或者应用程序的一部分,运行在用户空间。系统调用是操作系统的一部分,是内核为用户提供的程序接口,运行在内核空间中,许多库函数其实本质上使用系统调用来实现某些功能。

2 进程

2.1 进程的基本概念

进程本质上是描述程序的执行过程,是动态的。而程序其实是一些代码段,当启动应用程序时,比如启动QQ,会将QQ的代码放入内存,而执行QQ的过程就是进程,QQ应用叫做程序。

2.2 描述进程-PCB

为了使每个进程能够独立的运行,操作系统配置了一个专门的数据结构,叫作进程控制块(Process Control Block,PCB)来描述进程。Linux中的PCB是:task_struct

task_struct内容分类:

  • 标识符:用于描述当前进程唯一的标识符(PID),用来区别其他进程。
  • 状态:任务状态,退出代码,退出信号等。
  • 优先级:相对于其他进程的优先级。
  • 程序计数器:程序即将被执行的下一条指令的地址。
  • 内存指针:指向不同区域,比如栈区、堆区的起始和结束位置的指针,还有和其他进程共享的内存块的指针
  • 上下文数据:进程切换过程中需要保留的数据,比如进程执行过程中寄存器的数据等等。
  • I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息:处理器时间总和。
  • 其他信息
2.3 查看Linux进程

可以使用ps来查看进程。最常用的是使用ps aux来查看进程。
在这里插入图片描述
在这里插入图片描述

2.4 通过系统调用获得进程标识符

系统调用

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

pid_t getpid(void);  // 获得当前进程的PID号
pid_t getppid(void); // 获得当前进程的父进程的PID号
  • 1
  • 2
  • 3
  • 4
  • 5

我们不仅可以通过ps命令,也可以通过系统调用来获取进程的标识符,也可以获得父进程的标识符。

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

int main()
{
	printf("pid : %d\n",getpid());
	printf("pid : %d\n",getppid());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
2.5 通过系统调用创建进程

系统调用

pid_t fork(void);
  • 1
  • fork的作用是创建当前进程的子进程。
  • 返回值:
    • 成功:子进程中返回 0 ,父进程中返回子进程 ID
    • 失败:返回-1,并设值errno
  • 失败的原因:
    1. 当前系统的进程数已经达到系统规定的上限,这时errno会被设置为EAGAIN
    2. 系统内存不足,errno会被设置为ENOMEM

那么,fork为什么能够返回不同的返回值,并且,创建了子进程与父进程又有什么关系呢?

fork会拷贝当前进程的内存,并创建一个新的进程。如下图,fork函数会将整个进程的内存镜像拷贝到新的内存地址,包括代码段、数据段、堆栈以及寄存器内容。之后,我们就有了两个拥有完全一样内存的进程。fork系统调用在两个进程中都会返回,在父进程中,fork系统调用会返回子进程的pid。而在新创建的进程中,fork系统调用会返回0。所以即使两个进程的内存是完全一样的,我们还是可以通过fork的返回值区分旧进程和新进程。

在这里插入图片描述

代码演示

int main()
{
    pid_t pid = fork();
    if(pid == 0)
    {
        printf("子进程被创建\n");
    }
    else if(pid > 0)
    {
        printf("父进程创建子进程\n");
    }
    else
    {
        perror("fork");
        exit(0);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

通过fork的返回值来表示当前的是执行父进程还是子进程的代码,具体演示如下:

在这里插入图片描述

2.6 进程状态

进程状态是用来描述当前进程处于什么状况,在Linux内核源码中,有以下几种状态:

static const char * const task_state_array[] = {
    "R (running)", /* 0 */
    "S (sleeping)", /* 1 */
    "D (disk sleep)", /* 2 */
    "T (stopped)", /* 4 */
    "t (tracing stop)", /* 8 */
    "X (dead)", /* 16 */
    "Z (zombie)", /* 32 */
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

R(running):运行状态,表面当前进程处于运行队列中,正在使用CPU资源。

S(sleeping):睡眠状态,意味着进程正在等待事件完成,这里的睡眠也可以被称为可中断睡眠。

D(disk sleep):磁盘睡眠状态,也叫做不可中断睡眠状态,这个进程实在等待I/O资源结束。

T(stopped):停止状态,可以发送信号SIGSTOP来停止当前进程,可以发送SIGCONT信号让进程继续运行。

X(dead):死亡状态,这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

Z(zombie):僵尸状态,表面处于僵尸进程,通常是子进程退出时,父进程没有回收子进程资源,就会导致子进程处于僵尸状态。

那么,这些状态之间有什么关系呢?我们用一张图来描述:

在这里插入图片描述

同样,我们可以使用ps aux来查看当前进程的状态,如果在状态字符后面如果带+(如S+),表示进程是前台运行,否则是后台运行。

2.7 僵尸进程
  • 僵尸进程是一个比较特殊的状态。当进程退出并且父进程没有读取子进程退出的状态码就会产生僵尸进程。
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 如果父进程一直还在运行,僵尸进程就会一直存在,并且占用系统资源。

下面是一个导致僵尸进程的案例:

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

int main()
{
    pid_t pid = fork();
    if(pid < 0)
    {
        // 表示fork失败
        perror("fork");
        exit(1);
    }
    else if(pid == 0)
    {
        // 子进程
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5); // 睡眠5秒
        exit(EXIT_SUCCESS); // 成功退出,退出码为0
    }
    else
    {
        // 父进程
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(30);
    }
    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

先来分析一下这段代码,我们发现子进程在睡眠五秒后就会通过exit()退出子进程,但是父进程仍在进行。那么,我们执行该程序,并用ps来监视进程,会发生什么状态。

在这里插入图片描述

在这里插入图片描述

分别是子进程睡眠结束前和睡眠结束后的进程状态,我们发现子进程的pid:15527有S+变成了Z+,而Z状态表示的就是僵尸进程,但因为父进程仍为结束,并且子进程一直在等待父进程回收子进程的资源。

僵尸进程的危害

  • 我们知道子进程如果处于僵尸状态,父进程退出之后,子进程也跟着退出,但如果有一种情况,父进程永不退出,那这时,如果没有对子进程进行回收资源,就会造成内存泄漏的情况。
  • 父进程如果一直不读取,子进程就会一直保持Z状态。
2.8 孤儿进程
  • 孤儿进程指的是父进程优先于子进程退出,子进程就叫做"孤儿进程"。
  • 孤儿进程会被1号进程领养,来负责回收子进程的资源。

同样,我们看到下面这段案例代码:

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

int main()
{
        pid_t pid = fork();
        if (pid < 0)
        {
                perror("fork");
                return 1;
        }
        else if (pid == 0)
        { 
                // child
                printf("I am child, pid : %d\n", getpid());
                sleep(10);
        }
        else
        { 
                // parent
                printf("I am parent, pid: %d\n", getpid());
                sleep(3);
                exit(0);
        }
        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

先来分析这段代码,这段代码是父进程的睡眠时间比子进程的少,也就意味着父进程要先优于子进程退出。
在这里插入图片描述

我们发现该进程的父进程已经变成了一号进程。

2.9 进程的优先级
  • CPU资源的分配的顺序取决于进程的优先级
  • 优先级高的进程会先调度,对于多任务的系统来算,合理的分配优先级可以提高改善性能。
  • 也可以把不重要的进程安排在指定CPU上,可以大大改善系统的整体性能。

在这里插入图片描述

当我们执行ps -l的时候,我们可以看到一些信息

**UID:**表示执行者的身份

**PID:**表示这个进程的ID

PPID: 表示这个进程的父进程ID

**PRI:**表示这个进程的优先级,值越小的越先被执行

**NI:**代表这个进程的nice值

​ 那么,这个nice值到底是什么呢?与进程的优先级有什么关系?

  • PRI能够决定进程的优先级,这个是操作系统分配给我们的,我们无权修改。
  • NI也就是所谓的nice值,表示这个进程可以被修正的数值。
  • NI的取值范围是-20至19,也就是四十个级别。

那么,如何修正NI值呢?

  • 输入top命令
  • 按下"r"键,并且输入要修改的进程ID,在输入要修改的nice值。
2.10 环境变量

先来理解一下什么是环境变量?

  • 环境变量一般是操作系统中用来指定操作系统运行环境的一些参数。
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但
    是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

常见的环境变量

  • PATH:指定命令的搜索路径
  • HOME:用户的主要工作目录
  • SHELL:当前的shell,通常是/bin/bash。

获取环境变量的方法

  1. 使用命令的方式:echo $环境变量名

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传在这里插入图片描述

  1. 使用代码的方式,main函数的第三个参数env

    #include <stdio.h>
    int main(int argc, char *argv[], char *env[])
    {
        int i = 0;
        for(; env[i]; i++){
        printf("%s\n", env[i]);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    (img-ukjbg4XY-1664018584582)(C:\Users\17660\AppData\Roaming\Typora\typora-user-images\image-20220924184411802.png)]

  1. 访问特定的环境变量:getenv(环境变量名);

    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
        printf("%s\n", getenv("PATH"));
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

设置环境变量的方法

  1. 通过命令的方式设置:

    [chakming@VM-8-2-centos ~]$ name=zhangsan
    [chakming@VM-8-2-centos ~]$ export name
    [chakming@VM-8-2-centos ~]$ echo $name
    zhangsan
    
    • 1
    • 2
    • 3
    • 4
  2. 通过程序代码的方式:int setenv(const char *name, const char *value, int overwrite);

    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
        setenv("name","zhangsan",1);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

3 程序地址空间

我们来回顾一下C语言,当时对内存的概念并不清晰,可能只是简单的了解,实际上的内存布局是这样子的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KKqdsPBN-1660189398310)(C:\Users\17660\AppData\Roaming\Typora\typora-user-images\image-20220811105130627.png)]

然后,我们在来看下,在使用了fork的情况下,会出现什么情况?

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
        pid_t id = fork();
        if (id < 0)
        {
                perror("fork");
                return 0;
        }
        else if (id == 0)
        { 
            	// child
            	printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
        }
        else
        { 
            	// parent
            	printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
        }
        sleep(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
  • 23
  • 24
  • 25

结果为

在这里插入图片描述

其实这段代码,就是父进程和子进程同时去读取全局变量,因为我们直到fork()之后,子进程是复制了父进程,但是子进程和父进程并未对其修改。所有这里的值和地址都是一样的。

但是稍作修改之后:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
        pid_t id = fork();
        if (id < 0)
        {
        	 perror("fork");
                return 0;
        }
        else if (id == 0)
        { 
                // child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
                g_val = 100;
                printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
        }
        else
        { 
                // parent
                sleep(3);
                printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
        }
        sleep(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
  • 23
  • 24
  • 25
  • 26
  • 27

结果为

在这里插入图片描述

先观察这段代码的变动,我们在子进程里面修改了全局变量,然后父进程睡眠三秒,保证在子进程结束之后父进程在结束。但是,为什么父子进程的全局变量的地址是一样的,但他们的值却是不一样。

其实,这里是因为操作系统采用了虚拟地址存储技术。简单来说:

  • 我们一直以来说的内存其实是一种虚拟内存,那么对应的也叫做虚拟地址。而物理地址,也就是我们的硬件实际的地址,我们是看不到的,是操作系统来帮我们管理。
  • 那么,也就很好的理解上面为什么同样的地址,值却是不一样,这是因为,我们看到的是虚拟地址,而他们实际的物理地址是不一样的。
  • 那么,linux操作系统采用分页存储管理的方式来实现虚拟内存。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/273203
推荐阅读
相关标签
  

闽ICP备14008679号