加入收藏 | 设为首页 | 会员中心 | 我要投稿 鞍山站长网 (https://www.0412zz.com/)- 应用安全、运维、云计算、5G、云通信!
当前位置: 首页 > 服务器 > 搭建环境 > Linux > 正文

Linux系统编程之进程概念

发布时间:2022-08-09 10:21:38 所属栏目:Linux 来源:互联网
导读:注:本文部分图片来源于网络,如有侵权,请告知删除 1. 什么是进程?# 在了解进程概念之前,我们需要先知道程序的概念。 程序,是指编译好的二进制文件,这些文件在磁盘上,并不占用系统资源。 进程,指的是一个程序的执行实例,是操作系统分配系统资源的单位
注:本文部分图片来源于网络,如有侵权,请告知删除
 
1. 什么是进程?#
在了解进程概念之前,我们需要先知道程序的概念。
 
程序,是指编译好的二进制文件,这些文件在磁盘上,并不占用系统资源。
 
进程,指的是一个程序的执行实例,是操作系统分配系统资源的单位,这里的系统资源有CPU时间,内存等。当程序运行起来,产生一个进程。
 
也就是说,相比于程序,进程是一个动态的概念。
 
2. 用什么来描述进程?#
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。教材中称为PCB(process control block),不同的操作系统下有不同的PCB,Linux 下的进程控制块是 task_struct。
 
task_struct是Linux内核的一种数据结构,当一个进程创建时,系统会先将程序加载到内存,同时会将task_struct装载到内存中,在task_struct中包含着进程的信息。
 
task_struct的内容主要分为以下几类:
 
标示符(PID) : 描述本进程的唯一标示符,用来区别其他进程,本质上是一个非负整数。
 
进程状态: 任务状态,退出代码,退出信号等。
 
上下文数据: 进程执行时处理器的寄存器中的数据。
 
程序计数器: 程序中即将被执行的下一条指令的地址。
 
文件描述符表,包含很多指向 file 结构体的指针。
 
优先级: 相对于其他进程的优先级。
 
其他信息。
 
3. PID、PPID#
为了便于管理,操作系统中有父子进程的概念。子进程会继承父进程的属性和权限,而父进程也可以系统地管理子进程。
 
进程的标志符是PID,是进程的唯一标识,而父进程的标志符是PPID。
 
要查看进程的父子关系,可以用命令ps axj
 
我们在后台运行一个./test可执行文件,用如下命令查看该进程的父子信息
 
image-20210814122527856
 
可以看到,该进程的进程PID为7711,其父进程PPID为29455
 
要获取进程id和父进程id,可以使用getpid()和getppid()函数:
 
获取当前进程 ID pid_t getpid(void);
 
获取当前进程的父进程 ID pid_t getppid(void);
 
如运行如下代码后,可以输出该进程的id和父进程id
 
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
  printf("pid: %dn", getpid());
  printf("ppid: %dn", getppid());
  return 0;
}
输出结果:
 
image-20210814123657132
 
4. fork函数#
运行man 2 fork后,可以看到pid_t fork(void);
 
fork函数是用于创建子进程的一个函数,当父进程调用fork函数后,会创建一个子进程,父子进程代码共享,数据各自开辟空间。
 
一般情况下,fork之后通常要进行分流,如代码1
 
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int g_val = 0;
 
int main()
{
    pid_t id = fork();
    if(id < 0){
        perror("fork fail");
        return 1;
    }
    else if(id == 0) {
        //child
        printf("g_val = %d,child_pid = %d , &g_val = %pn",g_val,getpid(),&g_val);
    }
    else {
        //parent
        printf("g_val = %d,parent_pid = %d , &g_val = %pn",g_val,getpid(),&g_val);
    }
    return 0;
}
执行结果如下
 
image-20210814181301227
 
可以看出,分流之后,父进程执行的是id>0的代码,而子进程执行的是id == 0 的代码,也就是说,fork是有两个返回值的,如果子进程创建成功,fork给父进程返回的是子进程的PID,给子进程返回0。
 
需要注意的是,子进程执行的是fork之后的代码。这是为什么?
 
在父进程创建好子进程后,父子进程代码共有,父进程会将自己的数据拷贝给子进程,其中就包括了父进程程序计数器的值。程序计数器内存放的是程序中即将被执行的下一条指令的地址,由于父进程已经执行了fork前面的代码,因此子进程会和父进程一样,都执行fork之后的代码。
 
5. 进程的状态#
当一个进程实体从磁盘加载到内存时,会创建对应的task_stuct,进程有不同的状态。在Linux中,所有运行在系统里的进程都以task_struct链表的形式存在内核里,根据状态的不同,可以将
 
task_struct中有关于进程状态的描述:
 
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 */
};
R状态:可执行状态,只有该状态的进程才可以上处理机运行。同一时刻可以有多个进程同时处于R状态,除了上处理机的进程外,其余R状态的进程以链表的形式组成队列,等待上处理机。在操作系统教材中的运行态和就绪态,在Linux中统一为R状态。
 
S状态:可中断睡眠状态,进程因为等待某些资源,而没有上处理机运行,该状态即S状态。当得到等待的资源,或者接收到某些异步信号时,进程将会被唤醒。一般情况下用ps命令查看进程状态,大多数进程都是S状态。
 
D状态:深度睡眠状态,该状态下不接受一些异步信号。该状态存在的原因是操作系统的某一些操作要求是原子操作,中间不可以接受其他异步信号的干扰,只要对应资源不得到满足,就一直处于D状态。例如, kill -9 也杀不死D状态的进程。而实际中,我们用ps命令几乎是无法捕捉到D状态的进程,因为原子操作往往比较短暂。
 
T状态:可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
 
X状态:死亡状态,该状态是返回状态,在任务列表中看不到。
 
Z状态:僵尸状态,该状态是一个特殊的状态。当进程退出时,如果父进程没有读取到子进程退出的返回代码,就会产生僵尸进程。僵尸进程会一直以Z状态留在进程表中,等待父进程读取其退出状态。即便是退出状态的进程,本身也需要用PCB进行维护,也就是说,如果父进程不读取子进程的退出信息,子进程的PCB会一直在内存中,从而造成了内存泄漏。
 
除了僵尸进程,系统中还可能存在另外一种进程——孤儿进程。当父进程先退出时,子进程就成了孤儿进程,此时孤儿进程会被1号init进程领养,其PPID变为1。
 
6. 进程地址空间#
我们将第4节讲解fork函数时的代码稍作修改
 
#include <stdio.h>                             
#include <sys/types.h>    
#include <unistd.h>    
    
int g_val = 0;    
    
int main()    
{    
    pid_t id = fork();    
    if(id < 0){    
        perror("fork fail");    
        return 1;    
    }    
    else if(id == 0) {    
        //child   
        g_val = 10000;
        printf("g_val = %d,child_pid = %d , &g_val = %pn",g_val,getpid(),&g_val);    
    }    
    else {
        //parent
        sleep(3);//这段代码让父进程休眠3s,保证子进程的代码先执行,让子进程修改g_val
        printf("g_val = %d,parent_pid = %d , &g_val = %pn",g_val,getpid(),&g_val);    
    }    
    return 0;    
}
执行结果如下
 
image-20210814183733642
 
我们惊奇地发现,父进程和子进程的&g_val是一样的,但是g_val居然不一样!
 
我们知道,相同的物理内存单元中不可能存储不同的两个数,也就是说,这里的地址并不是实际的物理地址,而是虚拟地址。那么,操作系统是如何管理进程的地址空间呢?
 
6.1 mm_struct#
对于操作系统而言,管理的方式是先用数据结构进行描述,再将数据结构进行组织。我们知道当一个进程创建时,会创建对应的PCB,在Linux中,task_struct中有一个结构体——struct mm_struct,这个结构体就是用来描述该进程虚拟地址的结构体。
 
mm_struct源码如下
 
struct mm_struct {
 
    //指向线性区对象的链表头
    struct vm_area_struct * mmap;       /* list of VMAs */
    //指向线性区对象的红黑树
    struct rb_root mm_rb;
    //指向最近找到的虚拟区间
    struct vm_area_struct * mmap_cache; /* last find_vma result */
 
    //用来在进程地址空间中搜索有效的进程地址空间的函数
    unsigned long (*get_unmapped_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);
 
       unsigned long (*get_unmapped_exec_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);
 
    //释放线性区时调用的方法,          
    void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
 
    //标识第一个分配文件内存映射的线性地址
    unsigned long mmap_base;        /* base of mmap area */
 
 
    unsigned long task_size;        /* size of task vm space */
    /*
     * RHEL6 special for bug 790921: this same variable can mean
     * two different things. If sysctl_unmap_area_factor is zero,
     * this means the largest hole below free_area_cache. If the
     * sysctl is set to a positive value, this variable is used
     * to count how much memory has been munmapped from this process
     * since the last time free_area_cache was reset back to mmap_base.
     * This is ugly, but necessary to preserve kABI.
     */
    unsigned long cached_hole_size;
 
    //内核进程搜索进程地址空间中线性地址的空间空间
    unsigned long free_area_cache;      /* first hole of size cached_hole_size or larger */
 
    //指向页表的目录
    pgd_t * pgd;
 
    //共享进程时的个数
    atomic_t mm_users;          /* How many users with user space? */
 
    //内存描述符的主使用计数器,采用引用计数的原理,当为0时代表无用户再次使用
    atomic_t mm_count;          /* How many references to "struct mm_struct" (users count as 1) */
 
    //线性区的个数
    int map_count;              /* number of VMAs */
 
    struct rw_semaphore mmap_sem;
 
    //保护任务页表和引用计数的锁
    spinlock_t page_table_lock;     /* Protects page tables and some counters */
 
    //mm_struct结构,第一个成员就是初始化的mm_struct结构,
    struct list_head mmlist;        /* List of maybe swapped mm's.  These are globally strung
                         * together off init_mm.mmlist, and are protected
                         * by mmlist_lock
                         */
 
    /* Special counters, in some configurations protected by the
     * page_table_lock, in other configurations by being atomic.
     */

(编辑:鞍山站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读