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的组成:
- 进程号
- 进程的状态,有就绪、运行、挂起、停止等状态
- 进程切换时需要保存和回复的一些 CPU 寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录
- UMASK 掩码
- 文件描述表,包含很多指向 File 结构体的指针
- 和信号相关的信息
- 用户 ID 和组 ID
- 会话和进程组
- 进程可以使用的资源上限
2 环境变量
环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:
- 字符串
- 有统一的格式: 名称=值,多个值用冒号分隔
- 值用来描述进程环境信息
- 存储形式: 与命令行参数类似,
char *[]
数组,数组名environ
,内部存储字符串,NULL
作为哨兵结尾- 使用形式: 与命令行参数类似
- 加载位置: 与命令行参数类似,位于用户区,高于
stack
的起始位置- 引入环境变量表,必须声明环境变量,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[]);
携带环境变量表和参数表
- argv[0] 是程序名,arg[1~n-1] 是携带参数,arg[n] 是 NULL 结束符
- 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 注意事项
- malloc 分配内存可以分配 0 字节,也可以将其释放,但映射区并不可以分配 0 字节
- 不能对 mmap 的返回值进行修改,否则 munmap 无法成功
- 打开文件只读,则不论是否开启映射物理设备,均不能在映射区进行写操作
- 创建映射区的权限要小于等于打开映射区文件的权限,映射区的创建中隐含着一次对映射区文件的读操作
- 最后一个参数的 offset 必须要是 4k 的整数倍,即 4096 的整数倍,页大小
- 映射区大小不能大于文件大小
- 文件描述符先关闭对读写映射区操作无影响,因为现在是通过映射区读写,不再使用文件句柄
unlink(filename) 函数,删除零食临时文件目录项,使之具备被释放条件
truncate() 和 ftruncate() 两个函数可用于改变文件长度
6.3.3 父子进程共享
父子进程共享的内容有:
- 共享打开的文件
- mmap 建立的映射区(但必须要使用 MAP_SHARED)
6.3.3 匿名映射区
mmap 足够方便,但问题在于每次建立映射一定要依赖一个文件才能实现,通常为了建立映射区要 open 一个 temp 文件,从创建好了再 unlink、close,比较麻烦。于是可以直接使用匿名映射来代替,借助标志位 MAP_ANONYMOUS
或 MAP_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);
- 注意的是 fd 需要配置为-1
- mmap 可用于非血缘关系进程通信
- 使用 memcpy() 函数可以拷贝结构体
memcp(map, $student, sizeof(student));
以上是关于Linux系统编程之进程概念的主要内容,如果未能解决你的问题,请参考以下文章
Linux 编程之进程篇:task_struct进程创建和退出
Linux 编程之进程篇:task_struct进程创建和退出