Linux系统编程之进程概念

Posted 乌有先生历险记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux系统编程之进程概念相关的知识,希望对你有一定的参考价值。

注:本文部分图片来源于网络,如有侵权,请告知删除

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可执行文件,用如下命令查看该进程的父子信息

可以看到,该进程的进程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: %d\\n", getpid());
 	printf("ppid: %d\\n", getppid());
 	return 0;
}

输出结果:

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 = %p\\n",g_val,getpid(),&g_val);
    }
    else {
        //parent
        printf("g_val = %d,parent_pid = %d , &g_val = %p\\n",g_val,getpid(),&g_val);
    }
    return 0;
}

执行结果如下

可以看出,分流之后,父进程执行的是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 = %p\\n",g_val,getpid(),&g_val);    
    }    
    else {
        //parent
        sleep(3);//这段代码让父进程休眠3s,保证子进程的代码先执行,让子进程修改g_val
        printf("g_val = %d,parent_pid = %d , &g_val = %p\\n",g_val,getpid(),&g_val);    
    }    
    return 0;    
}

执行结果如下

我们惊奇地发现,父进程和子进程的&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.
     */

    mm_counter_t _file_rss;
    mm_counter_t _anon_rss;
    mm_counter_t _swap_usage;

    //进程拥有的最大页表数目
    unsigned long hiwater_rss;  /* High-watermark of RSS usage */、
    //进程线性区的最大页表数目
    unsigned long hiwater_vm;   /* High-water virtual memory usage */

    //进程地址空间的大小,锁住无法换页的个数,共享文件内存映射的页数,可执行内存映射中的页数
    unsigned long total_vm, locked_vm, shared_vm, exec_vm;
    //用户态堆栈的页数,
    unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
    //维护代码段和数据段
    unsigned long start_code, end_code, start_data, end_data;
    //维护堆和栈
    unsigned long start_brk, brk, start_stack;
    //维护命令行参数,命令行参数的起始地址和最后地址,以及环境变量的起始地址和最后地址
    unsigned long arg_start, arg_end, env_start, env_end;

    unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

    struct linux_binfmt *binfmt;

    cpumask_t cpu_vm_mask;

    /* Architecture-specific MM context */
    mm_context_t context;

    /* Swap token stuff */
    /*
     * Last value of global fault stamp as seen by this process.
     * In other words, this value gives an indication of how long
     * it has been since this task got the token.
     * Look at mm/thrash.c
     */
    unsigned int faultstamp;
    unsigned int token_priority;
    unsigned int last_interval;

    //线性区的默认访问标志
    unsigned long flags; /* Must use atomic bitops to access the bits */

    struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
    spinlock_t      ioctx_lock;
    struct hlist_head   ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
    /*
     * "owner" points to a task that is regarded as the canonical
     * user/owner of this mm. All of the following must be true in
     * order for it to be changed:
     *
     * current == mm->owner
     * current->mm != mm
     * new_owner->mm == mm
     * new_owner->alloc_lock is held
     */
    struct task_struct *owner;
#endif

#ifdef CONFIG_PROC_FS
    /* store ref to file /proc/<pid>/exe symlink points to */
    struct file *exe_file;
    unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
    struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
    pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
    /* reserved for Red Hat */
#ifdef __GENKSYMS__
    unsigned long rh_reserved[2];
#else
    /* How many tasks sharing this mm are OOM_DISABLE */
    union {
        unsigned long rh_reserved_aux;
        atomic_t oom_disable_count;
    };

    /* base of lib map area (ASCII armour) */
    unsigned long shlib_base;
#endif
};

因此,进程地址空间实际上就是结构体mm_struct所描述的虚拟空间,每个进程都有自己的虚拟地址空间。每个进程的虚拟地址如下图所示。

在Linux中,采用分页存储的方式对内存进行管理。既然我们平时所看到的地址不是实际的物理地址,那就需要操作系统将虚拟地址映射为物理地址。操作系统是借助页表来实现虚拟地址和物理地址的映射的,页表的本质也是一个数据结构,最主要的两项就是进程的虚拟地址和实际物理地址的映射关系。

6.2 写时拷贝

在我们的代码中,当fork创建子进程时,会将父进程的mm_struct也拷贝给子进程,一开始,内存中只有一份g_val,当子进程修改g_val时,由于父子进程的数据是各自私有的,进程之间的执行应该具有独立性,因此子进程修改g_val不应该影响到父进程。此时就会发生写时拷贝,即子进程在内存中开辟一块新的空间,将修改后的值填入该空间,并且修改子进程页表中虚拟地址映射的实际物理地址。

因此,我们看到了上述相同虚拟地址中存储的数值不同的场景。

6.3 为什么要有进程地址空间?

这是因为引入了进程地址空间后,可以保证每个进程所用的空间独立而连续3。一个进程的越界操作并不会影响另一个进程,这样就实现了内存的保护。同时,每个进程地址空间是远大于实际内存空间的,这样也可以通过虚拟的方式实现内存的扩充。当一个进程退出后,我们只需要清除掉该进程的mm_struct和页表就可,有利于内存的分配回收。

初探 Linux 系统编程之进程

0 前言

本文对 Linux 系统编程的进程相关知识进行总结,包含了进程的创建方法、IPC 实现等。

1 进程相关概念

  • 单道程序设计模式: DOS 操作系统
  • 多道程序设计模式: 通过时钟中断在硬件级别控制 CPU 轮转
  • MMU: 内存管理单元,位于 CPU 内部,完成虚拟内存与物理内存的映射和设置修改内存访问级别
  • PCB: 进程控制块,定义在 /usr/src/linux-haeders-3.16.0-30/include/linux/sched.h

    查看资源上限的命令: ulimit -a

  • PCB的组成:

    1. 进程号
    2. 进程的状态,有就绪、运行、挂起、停止等状态
    3. 进程切换时需要保存和回复的一些 CPU 寄存器
    4. 描述虚拟地址空间的信息
    5. 描述控制终端的信息
    6. 当前工作目录
    7. UMASK 掩码
    8. 文件描述表,包含很多指向 File 结构体的指针
    9. 和信号相关的信息
    10. 用户 ID 和组 ID
    11. 会话和进程组
    12. 进程可以使用的资源上限

2 环境变量

环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:

  • 字符串
  • 有统一的格式: 名称=值,多个值用冒号分隔
  • 值用来描述进程环境信息
  1. 存储形式: 与命令行参数类似,char *[] 数组,数组名 environ,内部存储字符串,NULL 作为哨兵结尾
  2. 使用形式: 与命令行参数类似
  3. 加载位置: 与命令行参数类似,位于用户区,高于 stack 的起始位置
  4. 引入环境变量表,必须声明环境变量,extern char ** environ

2.1 常见的环境变量

  • PATH: 可执行文件的搜索路径,从前往后搜索,所以新版本的环境变量因放置在前面
  • SHELL: 记录当前使用的命令解释器,如 /bin/bash
  • HOME: 当前主目录
  • LANG: 当前语言
  • TERM: 当前的终端信息

echo $PATH 打印当前的 PATH 变量

2.2 相关函数

  • char* getenv(const char *name); 成功:返回环境变量到值,失败:NULL
  • int setenv(const char *name, const char *value, int overwrite); 成功:0,失败:-1,override 取1表示覆盖原环境变量值
  • int unsetenv(const char *name); 成功:0,失败:-1,当name不存在是仍然返回0,当name命名为”ABC=”时则会出错

通过 man [函数名] 可以查看函数相关 API

3 进程控制

3.1 fork 函数

pid_t fork(void),创建一个子进程,返回值有两个(一个进程变为两个进程,各自的 fork() 都返回):返回子进程的 PID(非负整数)和返回 0。可以判断返回值确定子进程执行的代码或是父进程执行的代码

3.2 创建多个子进程

使用以下语句

for (i=0; i<n; i++) 
    fork();

并不是创建 N 个子进程,而是 (2^N-1)个子进程,正确的做法是在循环体中判断,如果是子进程(返回值=0),那么就 break

ps aux 显示所有进程
unistd.h 是 UNIX 系统标准库头文件
vim下使用:vs可以分屏

3.3 补充函数

  • uid_t getuid(void),获取当前进程的实际用户 ID
  • uid_t geteuid(void),获取当前进程的有效用户 ID
  • gid_t getgid(void),获取当前进程使用组 ID
  • gid_t getegid(void),获取当前进程有效用户组 ID

3.4 进程共享

父子进程共享之后的异同:
相同点:

  • 全局变量
  • .data .text
  • 栈、堆
  • 环境变量、信号处理方式
  • 用户ID、宿主目录、进程工作目录

不同点:

  • 进程 ID、父进程 ID
  • fork 返回值
  • 进程运行时间
  • 定时器
  • 未决信号集

注意:
1. 子进程并非将空间完全拷贝一份,而是遵循读时共享写时复制的原则
2. 父子进程共享文件描述符(所以进程通信可以通过文件共享方式实现)和 MMAP 建立的映射区

3.5 GDB 调试

第一步,在 gcc 编译选项中增加 -g 选项;第二步,gdb 运行程序
通过 set follow-fork-mode child 跟踪子进程,通过 set follow-fork-mode parent 跟踪父进程,默认跟踪父进程

4 Exec 函数族

fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。
将当前进程的.text、.data 替换为所要加载的.text、.data,然后让进程从新的.text 第一条指令开始执行,但进程 ID 不变,换核不换壳。
其中有六种以 exec 开头的函数,统称 exec 函数:

  • int execl(const char *path, const char *arg, …); // list
    加载一个进程,通过路径+程序名来加载,成功无返回,失败返回-1,;对比execlp,如加载”ls”命令带有-l,-F参数
execlp("ls", "ls", "-l", "-F", NULL); // 使用程序名在PATH中搜索
execl("/bin/ls", "ls", "-l", "-F", NULL); //使用参数1给出的绝对路径搜索
  • int execlp(const char *file, const char *arg, …); // list path
    加载一个进程,借助 PATH 环境变量,成功无返回,失败返回-1;参数1:要加载到程序的名字,该函数通常用来调用系统程序,如:ls、date、cp、cat 等命令

  • int execle(const char *path, const char *arg, …, char *const envp[]); // list environment
    借助环境变量表

  • int execv(const char *path, char *const argv[]);
    携带参数表
  • int execvp(const char *file, char *const argv[], char *const envp[]);
    携带环境变量表和参数表
  1. argv[0] 是程序名,arg[1~n-1] 是携带参数,arg[n] 是 NULL 结束符
  2. exec 族函数只在失败时才有返回值,成功无返回值,也不会继续再执行下面的程序

DUP2

引入: 将当前的进程信息输出到文件

  • 方法一: 通过 ps aux > out.txt 命令可以实现,但是 > 符并不属于参数,需要转义才可以
  • 方法二: 使用 DUP2 函数实现文件输出拷贝
int dup2(int oldfd, int newfd);

将输出指针 oldfd 复制到 newfd,即 newfd 所指向的文件和 oldfd 所指向的文件是一样的,也就实现了 newfd 重定向到 oldfd。

需要添加头文件 fcntl.h

5 回收子进程

  • 孤儿进程
    父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为 init 进程,称为 init 进程领养孤儿进程
  • 僵尸进程
    进程终止,父进程尚未回收,子进程残余资源(PCB)存放于内核中,变成僵尸(Zombie)进程

特别注意,僵尸进程是不能使用 kill 命令清除掉的,因为 kill 命令只是用来终止进程的,而僵尸进程已经终止。
ps aux 命令显示的进程列表中,STATE 栏表示当前状态,R 表示运行,S 表示后台运行,Z 表示僵尸进程

5.1 Wait 函数

一个进程在终止时会关闭所有的文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号信息。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息,然后彻底清除这个进程。我们知道一个进程的退出状态可以在 Shell 中用特殊变量$?查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出状态,同时彻底清除掉这个进程。

pid_t wait(int *status);

成功返回清理掉的子进程 ID,失败返回-1(没有子进程)
父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:

  • 阻塞等待子进程退出
  • 回收子进程残余资源
  • 获取子进程结束状态(退出原因)

当进程终止时,操作系统的隐式回收机制会完成:

  • 关闭所有文件描述符
  • 释放用户空间分配的内存
    内核的 PCB 仍存在,其中保存该进程的退出状态(正常终止->退出值,异常终止->终止信号)。

可使用 wait 函数传出参数 status 来保存进程的退出状态,借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:

  • WIFEXITED(status) [wait if exited] 非零 进程正常退出,使用 WEXITSTATUS(status) 可获取进程退出状态(exit的参数)
  • WIFSIGNALED(status) 非零 进程异常终止,使用 WTERMSIG(status) 取得使进程终止的那个信号的编号
  • WIFSTOPPED(status) 非零 进程处于暂停状态,使用 WSTOPSIG(status) 取得使进程暂停的那个信号的编号,WIFCONTINUED(status) 如果为真说明进程暂停后已经继续运行

5.2 waitpid 函数

作用同 wait,但可以指定 pid 进程清理,可以不阻塞

pid_t waitpid(pid_t pid, int *status, in)

成功返回清理掉的子进程 ID,失败返回-1(无子进程)
参数 pid:

  • >0 回收指定 ID 的子进程
  • -1 回收任意进程(相当于 wait)
  • 0 回收和当前调用 waitpid 一个组的所有子进程
  • <-1 回收指定进程组内的任意子进程
    参数3:

  • 0,阻塞回收

  • WNOHANG,非阻塞回收(一般使用轮询)

返回值:

  • pid 成功
  • -1 失败
  • 0 参数3为WNOHANG 并且子进程尚未结束

一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环

6 进程间通信

Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)
在进程间完成数据传递需要借助操作系统提供特殊的方法,如文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用,现今常用的进程间通信方式有:

  • 管道(使用最简单)
  • 信号(开销最小)
  • 共享映射区(无血缘关系)
  • 本地套接字(最稳定)

6.1 管道 PIPE

6.1.1 概念

管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一个管道,有如下特质:

  • 其实质是一个伪文件(实为内核缓冲区)
  • 由两个文件描述符引用,一个表示读端,一个表示写端
  • 规定数据从管道的写端流入管道,从读端流出

管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4K)实现
管道的局限性:

  • 数据自己读不能自己写
  • 数据一旦被读走,便不在管道中存在,因此,数据只能在一个方向上流动
  • 只能在有公共祖先的进程间使用管道

常见的通信方式有,单工通信、半双工通信、半双工通信

Linux 中的7中文件类型
- 文件 d 目录 l 符号链接 s 套接字 b 块设备 p 管道
前三种才占用存储空间,后四种称之为伪文件

6.1.2 pipe 函数

int pipe(int pipefd[2]);

成功: 0;失败: -1
函数调用成功会在传入参数返回 r/w 两个文件描述符,无需 open,但需 close

6.2 有名管道 FIFO

6.3 共享内存 MMAP

使用文件进行进程间通信

6.3.1 mmap 函数

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

成功,返回创建的映射区首地址;失败,返回 MAP_FAILED 宏
参数:

  • addr 建立映射区的首地址,由 Linux 内核指定,使用时,直接传递 NULL
  • length 域创建映射区的大小
  • prot 映射区权限 PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
  • flags 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
    MAP_SHARED 会将映射区所做的操作反映到物理设备(磁盘)上
    MAP_PRIVATE 映射区所做的修改不会反映到物理设备
  • fd 用来建立映射区的文件描述符
  • offset 映射文件的偏移(4K的整数倍)

6.3.2 注意事项

  1. malloc 分配内存可以分配 0 字节,也可以将其释放,但映射区并不可以分配 0 字节
  2. 不能对 mmap 的返回值进行修改,否则 munmap 无法成功
  3. 打开文件只读,则不论是否开启映射物理设备,均不能在映射区进行写操作
  4. 创建映射区的权限要小于等于打开映射区文件的权限,映射区的创建中隐含着一次对映射区文件的读操作
  5. 最后一个参数的 offset 必须要是 4k 的整数倍,即 4096 的整数倍,页大小
  6. 映射区大小不能大于文件大小
  7. 文件描述符先关闭对读写映射区操作无影响,因为现在是通过映射区读写,不再使用文件句柄

unlink(filename) 函数,删除零食临时文件目录项,使之具备被释放条件
truncate() 和 ftruncate() 两个函数可用于改变文件长度

6.3.3 父子进程共享

父子进程共享的内容有:

  • 共享打开的文件
  • mmap 建立的映射区(但必须要使用 MAP_SHARED)

6.3.3 匿名映射区

mmap 足够方便,但问题在于每次建立映射一定要依赖一个文件才能实现,通常为了建立映射区要 open 一个 temp 文件,从创建好了再 unlink、close,比较麻烦。于是可以直接使用匿名映射来代替,借助标志位 MAP_ANONYMOUSMAP_ANON,注意该宏仅在 Linux 操作系统中可用

MAP_ANON 宏仅在 Linux 操作系统中可用,在类 Unix 系统中如果没有该宏,可以使用 fd = open(“/dev/zero”, O_RDWR) 代替

用法

int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
  1. 注意的是 fd 需要配置为-1
  2. mmap 可用于非血缘关系进程通信
  3. 使用 memcpy() 函数可以拷贝结构体
memcp(map, $student, sizeof(student));

以上是关于Linux系统编程之进程概念的主要内容,如果未能解决你的问题,请参考以下文章

Linux 编程之进程篇:task_struct进程创建和退出

Linux 编程之进程篇:task_struct进程创建和退出

Linux 编程之进程篇:task_struct进程创建和退出

初探 Linux 系统编程之进程

初探 Linux 系统编程之进程

Linux系统编程之进程间通信之浅谈信号