Linux操作系统下进程讲解
一、进程的基本概念:
1. 什么是进程
在传统的操作系统中,程序不可以独立的运行,作为资源分配和独立运行的基本单位都是进程。进程的定义是一个可执行中程序的实例,系统中每一个程序都运行在某个的上下文中。
上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的的内容、程序计数器、环境变量以及打开文件描述符集合。
2.进程的特征
2.1 结构特征
进程通常是不可以并发执行的,为使程序独立运行,必须配置进程控制块(PCB),由程序段、数据段和PCB共同组成进程实体,我们通常所说的进程就是进程实体,创建进程就是创建进程实体中的PCB。
2.2 动态性
进程的实质就是进程实体的一次执行过程,因此动态性是进程最基本的特征,具体表现在:“由创建而产生、调度而执行、撤销而消亡”。
2.3 并发性
指的是多个进程实体共同存在内存中且可以在一段时间内同时运行,并发性是进程的重要特征,也是操作系统重要特征,引入进程目的就是为了并发性。
2.4 独立性
进程实体是一个独立运行、独立分配资源和独立接受调度的基本单元。
2.5 异步性
进程实体按异步方式运行
通过进程的特征我们可以了解到进程是进程实体的运行过程,是系统源分配和独立运行的基本单元。
3. 进程的三种基本状态
3.1 就绪状态
进程已分配到处CPU以外的所有必须资源,只要获取到CPU则可以立即执行,可以有多个就绪态的进程,他们通常称为就绪队列。
3.2 执行状态
进程获得CPU,正在执行的进程,单处理器系统只有一个进程处于执行状态,多处理器可以有多个。
3.3 阻塞状态
正在执行的进程因为某事件而无法继续执行,放弃处理机而处于暂停的状态。
3.4 除了以上常见的状态外还有挂起状态(正在执行的进程暂停执行)、僵尸状态(子进程先退出,父进程没有为其收尸)。
4. 进程控制块
为了描述和控制进程,系统为每个进程定义了一个数据结构——进程控制块(PCB),PCB是对操作系统并发执行的进程进行控制和管理的,进程控制块主要包括以下四个信息:
a.进程标识符,b.处理机状态, c.进程调度信息,d.进程控制信息。
二、进程环境
了解Linux系统环境中C程序的环境是了解系统进程控制的前提,所以我们需要先了解一个进程是如何启动和终止的,如何向其传递参数表和环境以及存储空间布局和分配释放内存。
1. main函数
我们知道C程序总是从main开始执行,在调用main函数前需要做一些准备,首先调用一个特殊的启动例程,可执行文件将此例程指定为程序的起始地址,然后启动例程从内核取得命令行参数和环境变量值。
2. 进程终止
2.1 linux下有六种终止进程的方式,a b c 为正常返回,d e f为异常返回
a. 从main函数返回
b. 调用exit、_exit _Exit 函数
c. 最后一个线程从启动例程返回或调用pthread_exit函数
d. 调用abort 函数
e. 接到一个信号
f. 最后一个线程对取消请求做出响应。
不管进程如何终止,内核都会关闭所有打开的文件描述符,释放他们所使用的存储器。
2.2 退出函数
#include <unistd.h>
void _exit(int status);
功能:进程的退出
参数:0代表正常 退出时不刷新缓冲区
1 代表非正常
#include <stdlib.h>
void _Exit(int status);
void exit(int status);
功能:进程的退出
参数:0代表正常 退出时刷新缓冲区
1 代表非正常
2.3 C程序的存储空间布局
详细介绍(https://blog.csdn.net/qq_34934140/article/details/87903366)
C程序由以下几部分组成
* 正文段(只读段):CPU执行的机器指令部分,共享且只读。
* 初始化数据段(数据段):包含程序中明确赋值的全局变量和局部变量
* 未初始化数据段(bss段):存放未进行显示初始化的全局变量和静态变量,内核会将此段中数据初始化为0和空指针。
* 栈:由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。
* 堆:存放动态分配的数据,一般由程序员动态分配和释放,若程序员不释放,程序结束可能由系统自动回收。
* 共享库的内存映射区域:这是Linux动态链接器和其他共享库代码的映射区域
size 命令可以查看 正文段、数据段和bss段长度,dec 和 hex 分别以十进制和十六进制表示三段总长度。
linux@ubuntu:~$ size
text data bss dec hex filename
2478 312 8 2798 aee a.out
2.4 共享库
共享库使得可执行文件中不再需要包含公用的库函数,而只需在所以进程都可以引用的存储区中保存这种库的例程副本,程序在第一次执行或调用库函数时候,用动态
链接的方法与共享库链接,减少了可执行文件的长度但是增加了运行时间。
2.5 存储空间分配
详解(https://blog.csdn.net/qq_34934140/article/details/89285597)
#include <stdlib.h>
void *malloc(size_t size);
功能:分配指定字节的存储区,初始值不确定,需要用memset初始化。
参数:要开辟空间的大小
返回值:成功:开辟空间的首地址,失败NULL
void free(void *ptr);
功能:释放空间
参数:要释放的空间的首地址
返回值:无
void *calloc(size_t nmemb, size_t size);
功能:分配指定指定数量指定长度的存储区,不需要初始化,每一位都已经初始化为0。
参数:要开辟空间的大小
返回值:成功:开辟空间的首地址,失败NULL
void *realloc(void *ptr, size_t size);
功能:增加或减少已经分配内存的长度,新增区域初始值不确定。
参数:要开辟空间的大小
返回值:成功:开辟空间的首地址,失败NULL
2.6 环境变量
环境变量一般指在操作系统中用来指定操作系统运行环境的一些参数。字符串形式:name=value
#include <stdlib.h>
char *getenv(const char *name);
功能:获取环境变量
参数:环境变量名
返回值:指向name关联的value指针,未找到返回null。
int putenv(char *string);
功能:设置环境变量
参数:name=value的字符串,放到环境中,如果name存在则覆盖。
返回值:成功0 失败非0。
int setenv(const char *name, const char *value, int overwrite);
功能:设置环境变量
参数:name:环境变量名
value:新值
overwrite:非0,新值覆盖
0, 保持原值
返回值:成功0 失败非0。
int unsetenv(const char *name);
功能:设置环境变量
参数:删除name的定义
返回值:成功0 失败非0。
2.7 setjmp 和 longjmp 函数
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
在希望回到位置时调用setjmp,setjmp参数env 是一个特殊类型,存放着调用longjmp用来恢复栈状态的信息。
通常env是一个全局变量,这俩函数使用同一个env。对于longjmp函数第一个参数env 和setjmp的env一样,第二个参数val是一个非0的值,这个val值将是setjmp的返回值,通过
返回的该值来确定是哪里调用的longjmp,因为一个setjmp可以对应多个longjmp。
2.8 每个进程都有一组资源限制,我们可以使用getrlimit和setrlimit函数查询和修改。
#include <sys/time.h>
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
功能:获取/设置资源限制值
参数:rlim:struct rlimit {
rlim_t rlim_cur; /* Soft limit */
rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
resource:限制名,取以下值;
RLIMIT_AS:进程可以用最大存储空间长度,
RLIMIT_CORE:core文件最大字节数
RLIMIT_CPU:CPU时间最大量值,当超过软限制时发送SIGXCPU信号。
RLIMIT_DATA:数据段最大字节数,初始化和未初始化数据段以及堆的总和。
RLIMIT_FSIZE:可以创建文件的最大字节长度,超过软限制时发送SIGXFSZ信号
RLIMIT_MEMLOCK:一个进程使用mlock能够锁住在存储空间最大字节数。
RLIMIT_MSGQUEUE:进程消息队列可分配最大存储字节数
RLIMIT_NICE:进程调度优先级,nice值越小,优先级越高
RLIMIT_NOFILE:每个进程最多打开文件数,
RLIMIT_NPROC:每个实际用户ID可以拥有最多子进程数,
RLIMIT_RSS:内存中存储最多的字节数
RLIMIT_STACK:栈最大字节数
RLIMIT_SBSIZE:某一时刻用户可占有套接字缓冲区最大字节数。
返回值:成功0 失败非0
注释:任何一个进程都有一个可将软限制值更改为小于等于其硬限制值。
任何一个进程可以降低硬限制值,但必须大于等于软限制值,普通用户不可逆
只有超级用户才可以修改硬限制值
修改了资源限制之后,子进程将继承。
三、进程控制
进程控制是进程管理的基本功能,它用于创建、终止一个进程,还可以转换进程的状态,进程控制由操作系统内核中的原语实现的:由若干条指令组成,用于完成一定功能的过程。
1. 进程的创建
1)申请空白的PCB
2)为新进程分配资源
3)初始化进程控制块
4)将新进程插入就绪队列
2. 进程终止
1)正常终止
2)异常终止:越界错误、非法指令、超时、算术运算错误等等。
3)外界干扰:父进程终止、系统干预、发送kill信号
4)终止过程:根据进程号从PCB集合找到对应PCB,读出状态,若处于执行状态则终止执行,同时终止子孙进程,释放资源,移除队列。
3. 进程的阻塞与唤醒
1)请求服务:如果发出请求服务等不到回应则进入阻塞,直到得到回应唤醒
2)启动某种操作:启动了新的操作需要新操作返回才能继续进行,则进入阻塞,直到新操作返回唤醒
3)数据未到达:进入阻塞,直到数据到达唤醒
4)无新任务:进入阻塞,直到新任务到达唤醒
4. 进程函数
4.1 进程标识
每个进程都有一个唯一的非负整形进程ID且唯一。进程ID为0的是调度进程,被称为交换进程,该进程是内核的一部分,也被称为系统进程。进程ID为1的是
init进程,该进程文件在/etc/init(旧版本)或/sbin/init(新版本)。init进程通常读取与系统有关的初始化文件(/etc/rc*文件或/etc/inittab以及/etc/init.d中的文件),
是所有进程的孤儿进程的父进程。
获取进程号
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:获取进程ID
pid_t getppid(void);
功能:获取父进程ID
uid_t getuid(void);
功能:获取调用进程的实际用户ID
uid_t geteuid(void);
功能:获取调用进程的有效用户ID
gid_t getgid(void);
功能:获取调用进程的实际组ID
gid_t getegid(void);
功能:获取调用进程的有效组ID
4.2 创建进程
#include <unistd.h>
pid_t fork(void);
返回值:0:子进程
>0: 父进程ID
<0:错误
注释:fork之后子进程是父进程的副本,继承了父进程的数据空间、堆、栈以及父进程对文件的读写权限。在fork之后最好他们不需要的文件描述符,防止混乱。
fork一般失败是因为系统中进程过多,超过了限制。
4.3 终止进程
同本章的2.2节一样
4.4 返回进程终止状态
内核为每一个终止的子进程保存了一定量的信息,父进程可以调用wait和waitpid得到这些信息,包括进程ID、终止状态、进程使用CPU时间总量。当一个进程终止时就向
父进程发送SIGCHLD信号,父进程可以选择忽略也可以设置一个信号处理函数,系统默认是忽略。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
功能:在父进程中执行,阻塞等待子进程的退出,回收子进程的资源,如何子进程已经终止且是僵尸进程则立即返回。
参数:status 用于保存子进程退出的状态, NULL 不关心子进程状态
返回值:成功:退出子进程的pid , 失败 -1
pid_t waitpid(pid_t pid, int *status, int options);
功能:在父进程中执行,等待子进程的退出,回收子进程的资源
参数:pid > 0 等待指定的pid进程结束
= -1等待任意的子进程退出
= 0 调用 进程组ID的任一进程
status:用于保存子进程退出的状态, NULL 不关心子进程状态
options:WNOHANG 不阻塞等待 成功等到子进程pid,没有等到0,失败-1
0 阻塞的方式等待 等到返回子进程的pid,失败返回-1
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
功能:允许一个进程指定要等待的子进程,
参数:idtype: P_PID: 等待一特定进程, id:包含子进程的进程ID
P_PGID:等待特定进程组中的任一子进程, id:包含要等待子进程的进程组ID
P_ALL: 等待任一进程 id:忽略
infop: 指向siginfo的指针,包含了子进程的状态改变有关信号的详细信息。
options: WEXITED:等待已退出的进程
WSTOPPED:等待一进程,他已经停止,但状态尚未报告
WCONTINUED:等待一进程,它以前被停止,后来又继续,但状态尚未报告
WNOHANG:如果没有可用子进程退出状态则立即返回。
WNOWAIT:不破坏子进程退出状态,该子进程退出后可由后续的wait等函数取得。
返回值:成功:0 , 失败 -1
4.5 exec函数族
当进程调用exec函数时,该进程执行的程序完全替换为新进程,而新程序则从其main函数开始执行,因为调用exec并不创建新进程,所以进程ID并没有改变,
只是替换了当前进程的正文段、数据段、堆和栈。fork一个新进程后,可以用exec初始执行新程序,可以exit终止、wait等待。
#include <unistd.h>
extern char **environ; //环境变量
int execl(const char *path, const char *arg, ...);
功能:在一个进程中,启动执行另一个进程
参数: path表示可执行文件的路径或名字
arg 可变参数:表示传递给可执行文件的参数,参数可变
第一个参数必须与可执行文件名字一样,最后一个参数必须是NULL
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
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[]);
这些函数区别
a. path: 是取路径名作为参数, filename: 是以文件名作为参数,如果filename中有‘ / ’ 则视为路径名,否则就在PATH环境变量所指定的目录中搜索可执行文件。
b. 函数中含有字母 p 的表示该函数取filename为参数,并且用PATH环境变量寻找可执行文件。
c. 函数中含有字母 l 的表示该函数取一个参数表,与字母 v 互斥。
d. 函数中含有字母 v 的表示该函数取一个argv[]矢量,
e. 函数中含有字母 e 的表示该函数取envp[]数组而不使用当前环境。
4.6 更改用户ID和组ID
在Linux系统下,对所有文件的操作都是需要权限的,每个用户ID和组ID都有自己的权限,当访问某些资源权限不够需要增权或者不允许访问某些资源时候,
我们需要更改自己的用户ID或者组ID时候需要以下函数。但是并不是任何人都可以修改ID,有以下规则
a. 进程是超级用户
b. 不是超级用户,但是uid等于实际用户ID或者保存的设置用户ID。
#include <sys/types.h>
#include <unistd.h>
int setuid(uid_t uid);
功能:设置用户ID
参数:新uid
返回值:成功0 失败-1
int setuid(gid_t gid);
功能:设置组ID
参数:新gid
返回值:成功0 失败-1
#include <sys/types.h>
#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
功能:交换实际用户ID和有效用户ID的值
参数: ruid:实际用户ID
euid:有效用户ID
返回值:成功0 失败-1
int setregid(gid_t rgid, gid_t egid);
功能:交换实际组ID和有效组ID的值
参数: ruid:实际组ID
euid:有效组ID
返回值:成功0 失败-1
#include <sys/types.h>
#include <unistd.h>
int seteuid(uid_t euid);
功能:设置有效用户ID
参数:新euid
返回值:成功0 失败-1
int setegid(gid_t egid);
功能:设置有效组ID
参数:新egid
返回值:成功0 失败-1
4.7 system 函数
在终端输入命令很简单,但是如果想在代码中执行命了该怎么办呢?system函数就解决了这个问题
#include <stdlib.h>
int system(const char *command);
功能:执行命令
参数:command:命令对应的字符串
返回值:< 0:失败
4.8 进程会计
在Linux系统中提供了一个选项以进行进程会计处理,启用之后,每当进程结束时内核就会写一个会计记录,包含命令名、所使用CPU时间总量、用户ID和组ID,启用时间等。
这些数据由内核保存在进程表中,并在一个新进程创建时初始化,结束时写一个会计记录。我们不能获取永不终止的进程的会计记录例如init进程。记录顺序是按照进程终止顺序
记录的,不是启动顺序。
#include <unistd.h>
int acct(const char *filename);
4.9 获取用户登录名
#include <unistd.h>
char *getlogin(void);
4.10 进程调度
Linux系统对进程提供的只是基于调度优先级的粗粒度控制,调度策略和调度优先级是由内核确定的,进程可以通过nice值来选择优先级的等级,nice值越小优先级越高。
#include <unistd.h>
int nice(int inc);
功能:设置调度优先级
参数:inc: nice值
返回值: 成功:新的nice值,失败 -1.
#include <sys/time.h>
#include <sys/resource.h>
int getpriority(int which, int who);
功能:获取nice值,与nice函数不同是可以获取一组相关进程的nice值
参数:which:PRIO_PROCESS:进程
PRIO_PGRP:进程组
PRIO_USER:用户ID
who:一个或多个进程
返回值: 成功 nice值,失败 -1.
int setpriority(int which, int who, int prio);
功能:设置调度优先级
参数:which:PRIO_PROCESS:进程
PRIO_PGRP:进程组
PRIO_USER:用户ID
who:一个或多个进程
prio:nice新值
返回值: 成功:新的nice值,失败 -1.
四、进程关系
4.1 进程组
进程组是一个或多个进程的集合,同一进程组中的各进程接收来自同一终端的各种信号,每个进程组有自己的唯一进程组ID,也存放在pid_t中。每一个进程组有一个组长进程,
其进程ID等于进程组ID。
#include <unistd.h>
pid_t getpgrp(void); /* POSIX.1 version */
pid_t getpgrp(pid_t pid); /* BSD version */
功能:获取进程组ID
参数:pid:进程组id
返回值: 成功:返回进程组ID,失败-1
int setpgrp(void); /* System V version */
int setpgrp(pid_t pid, pid_t pgid); /* BSD version */
功能:获取进程组ID
参数:pid:原进程组id,pgid:新进程组ID
返回值: 成功:0,失败-1
4.2 会话
一个或者多个进程组的集合,一个会话可以有一个控制终端,这通常是终端设备,建立与控制终端连接的会话首进程被称为控制进程,一个会话中的几个进程组可以被分
为一个前台进程组和多个后台进程组。无论何时输入中断键(ctrl + c)都会发送给前台进程组的所有进程。
#include <unistd.h>
pid_t setsid(void);
功能:创建一个新会话
返回值:成功:返回进程组ID,失败-1
注释:如果调用此函数的进程不是进程组组长,则创建一个新会话。该进程会变成新会话首进程,该进程成为新进程组组长,进程组ID是该进程ID,该进程没有控制终端。
pid_t getsid(pid_t pid);
功能:获取会话首进程的进程组ID
返回值:成功 会话首进程的进程组ID, 失败 -1
#include <termios.h>
pid_t tcgetsid(int fd);
功能:返回控制终端会话首进程的会话ID
参数:控制终端的文件描述符
返回值:成功 会话首进程的进程组ID, 失败 -1
五、进程间通信
进程通信是指进程之间的信息交换,进程之间的互斥和同步,生产者以及消费者的问题。
5.1 管道
管道只能在具有公共祖先的两个进程之间使用且是半双工(可以创建两个管道来实现全双工),管道是最常用的IPC形式,通过pipe函数创建管道,通常先调用pipe,接着调用fork,从而创建从父进程到子进程的通道。
当管道一端被关掉时:
读端关闭:所以数据读出后返回0,表示文件结束,如果写端还有进程就不会产生文件的结束。
写端关闭:产生SIGPIPE信号,可以忽略或者捕捉,write返回-1。对于写管道内核规定了管道缓冲区最大值,最大值为PIPE_BUF,可以通过pathconf函数获取该值。
读写特性:
写:读端存在 1.管道中没有放满数据,写正常
2.管道放满数据,写阻塞,直到管道中有了空间,写继续
读端不存在:管道断裂,程序直接结算
读:写端存在:1.管道中没有数据,读阻塞
2.管道中有数据,读正常
写端不存在:1.管道中没有数据,读不阻塞,程序继续
2.有数据,正常读。
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建管道
参数:两个文件描述符,pipefd[0]为读打开(输入),pipefd[1]为写打开(输出)。
返回值:成功:0, 失败 -1.
#include <stdio.h>
FILE *popen(const char *command, const char *type);
功能:创建一个连接到其他进程的管道,然后读其输出或者向其发送数据
参数: command:command参数是一个指针,指向包含shell命令行的以空结束的字符串
type: 文件可读 r:文件指针连接到command的标准输出
文件可写 w:文件指针连接到command的标准输入
返回值:成功:返回文件指针, 失败null.
例:
fd = fopen("ls *.c" , "r")
int pclose(FILE *stream);
功能:关闭管道
参数:popen返回的FILE指针。
返回值:成功:返回command的终止状态, 失败 -1.
例子:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
int pipefd[2];
char buf[32];
ssize_t size;
if(pipe(pipefd) < 0)
{
perror("fail to pipe");
exit(1);
}
write(pipefd[1],"hello",6);
close(pipefd[1]);
size = read(pipefd[0],buf,sizeof(buf));
printf("size:%d\n",size);
printf("buf:%s\n",buf);
printf("*******\n");
return 0;
}
5.2 FIFO
FIFO被称为命名管道(有名管道),我们之前说的管道为未命名的管道(无名管道),无名管道只能在两个相关进程之间通信(有共同祖先),而FIFO可以在不相干的进程间通信。
FIFO是一种文件类型,在stat结构体中可以看到FIFO类型。
特性:1.一台机器上任意两个进程可以通信
2.全双工模式
3.可以利用文件io操作,不支持lseek
4.当创建一个FIFO后,使用open函数O_RDONLY,O_WDONLY,打开时,open函数会阻塞,直到另一个另一个进程以对应的方式打开。
5.当对有名管道进行写操作,如果管道已满,write函数会阻塞,当对管道进行读操作时,如果管道没有数据,read函数会阻塞
用途:
1. 将数据从一条管道传到另一条,无需中间件
2.客户-服务器进程应用程序,二者之间的数据传递。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能:创建管道
参数:filename 要创建的管道名
mode 管道访问权限(与open函数mode相同)
返回值:成功0 失败 -1
例子:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
int fd_r,fd_w;
char buf[32];
if(mkfifo("fifo_A",0666) < 0)
{
perror("fail to mkfifo");
exit(1);
}
if((fd_r = open("fifo_A",O_RDWR)) < 0)
{
perror("fail to open fd_r");
exit(1);
}
write(fd_r,"hello",6);
read(fd_r,buf,sizeof(buf));
printf("buf:%s\n",buf);
return 0;
}
5.3 System V IPC结构 (消息队列、信号量、共享存储器。)
每个内核中的IPC结构(消息队列、信号量、共享存储器)都有一个非负整数的标识符,从一个消息队列发送或者读取数据时候都需要一个队列标识符。与文件描述符不同的是,创建一个一个ipc增加1,但是删除不会减少1,直到
到达最大值,然后转到0。每一个IPC对象都与一个键(key)关联,该键作为该对象的外部名。创建一个IPC结构(消息队列、信号量、共享存储器)都需要一个键。
IPC结构在系统内起作用,没有引用计数,如果一个进程创建了一个消息队列,然后放了一些数据在队列里,然后终止,那么消息队列和内容会一直存在,直到调用删除函数或者命令。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:获得一个key值,用于system V IPC通信
参数: pathname 已经存在的文件的名字
proj_id 当产生键时,只使用proj_id的低八位(保证低八位不为0),
返回值:成功获得key值,失败-1
注释: 1、ftok根据路径名,提取文件信息,再根据这些文件信息及project ID合成key,该路径可以随便设置。
2、该路径是必须存在的,ftok只是根据文件inode在系统内的唯一性来取一个数值,和文件的权限无关。
3、proj_id是可以根据自己的约定,随意设置。这个数字,有的称之为project ID; 在UNIX系统上,它的取值是1到255;
关于ftok()函数的一个陷阱:在使用ftok()函数时,里面有两个参数,即fname和id,fname为指定的文件名,而id为子序列号,这个函数的返回值就是key,它与指定的文件的索引节点号和子序列号id有关,这样就会给我们一个误解,即只要文件的路径,名称和子序列号不变,那么得到的key值永远就不会变。
事实上,这种认识是错误的,想想一下,假如存在这样一种情况:在访问同一共享内存的多个进程先后调用ftok()时间段中,如果fname指向的文件或者目录被删除而且又重新创建,那么文件系统会赋予这个同名文件新的i节点信息,于是这些进程调用的ftok()都能正常返回,但键值key却不一定相同了。
由此可能造成的后果是,原本这些进程意图访问一个相同的共享内存对象,然而由于它们各自得到的键值不同,实际上进程指向的共享内存不再一致;如果这些共享内存都得到创建,则在整个应用运行的过程中表面上不会报出任何错误,然而通过一个共享内存对象进行数据传输的目 的将无法实现。!
所以要确保key值不变,要么确保ftok()的文件不被删除,要么不用ftok(),指定一个固定的key值。
5.4 查看IPC对象的命令
ipcs -m 查看共享内存的标识符
ipcs -q 查看消息队列的标识符
ipcs -s 查看信号灯集的标识符
删除IPC对象的命令
ipcrm -m 要删除的共享内存
ipcrm -q 要删除的消息队列
ipcrm -s 要删除的信号灯集
5.5 消息队列
是一个消息的列表,用户可以在消息队列中添加信息,可以读取信息,符合先进先出
消息队列的本质其实是一个内核提供的链表,内核基于这个链表,实现了一个数据结构,并且通过维护这个数据结构来维护这个消息队列。
向消息队列中写数据,实际上是向这个数据结构中插入一个新结点;从消息队列汇总读数据,实际上是从这个数据结构中删除一个结点。
5.5.1 创建或者打开一个消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
功能:创建或者打开一个消息队列
参数 key : ftok生成的键值 ftok的返回值
msgflg :IPC_CREAT 创建消息队列
PC_EXCL 如果消息队列已经存在,加了创建参数,报错
用法同open函数
返回值:成功返回消息队列的id,失败返回-1
5.5.2 添加信息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:向一个消息队列中发送一条消息
参数:msqid: 消息队列的id,msgget函数的返回值
msgp: 要发送的消息
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
msgsz:要发送的消息正文的大小 sizeof(msgbuf)- sizeof(long)
msgflg :0 如果队列中的消息满了,它会阻塞等到空间有要发送的消息。
IPC_NOWAIT 如果队列中的消息满了,它不会阻塞,立即返回EAGAIN
返回值:成功0 失败-1
5.5.3 读取信息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
功能:从消息队列中读取一条消息
参数:msqid: 消息队列的id,msgget函数的返回值
msgp: 读取到消息所存放的位置(必须和放的时候结构体长的一样)
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
msgsz: 要发送的消息正文的大小 sizeof(msgbuf)- sizeof(long)
msgtyp: 0 表示从第一个开始读取,按队列的规则
>0 表示按指定的type类型进行读取
<0 表示和它的绝对值相等的类型进行读取(接收消息队列中类型不小于msgtype的绝对值又最小的消息)
msgflg: 0 阻塞的方式进行读
IPC_NOWAIT 以非阻塞的方式
返回值:成功返回读取正文的长度
5.5.4 删除消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:控制消息队列
参数:msqid 消息队列id
cmd: IPC_STAT 表示获取消息队列的属性,获取到的内容存放在第三个参数
IPC_SET 表示设置消息队列的属性,设置的内容在第三个参数存储
IPC_RMID 删除消息队列,第三个传NULL
buf 一般为NULL
返回值:成功0,失败-1
注释:删除队列后,仍然使用该消息队列的进程下次对进程进行操作时会返回错误。
5.5.5 demo
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <string.h>
struct msgbuf
{
long mtype;
char buf[32];
};
int main(int argc, const char *argv[])
{
key_t key;
int msgid;
struct msgbuf msg,msg_tmp;
key = ftok(".",'a');
if(key == -1)
{
perror("fail to ftok");
exit(1);
}
msgid = msgget(key,IPC_CREAT|IPC_EXCL|0666);
if(msgid == -1)
{
if(errno == EEXIST)
{
msgid = msgget(key,0666);
}
else
{
perror("fail to msgget");
exit(1);
}
}
msg.mtype = 200;
strcpy(msg.buf,"hello world");
if(msgsnd(msgid,(void *)&msg,sizeof(msg) - sizeof(long),0) < 0)
{
perror("fail to msgsnd");
exit(1);
}
msgrcv(msgid,&msg_tmp,sizeof(msg_tmp) - sizeof(long),200,0);
printf("msg mtype= %ld,buf=%s\n",msg_tmp.mtype,msg_tmp.buf);
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
5.5.6 影响消息队列的系统限制(以Linux 3.2.0 为例)
可发送最长消息字节数 8192
一个特定队列的最大字节数 16384
系统中最大消息队列数 16
5.6 信号量
一般用于进程同步,信号量维持着一个计数器,当计数器值为0时候,进程进入休眠,大于0时候,进程被唤醒,所以当该值为正时候,每使用一个资源单位,该值减一。
5.6.1 影响信号量的系统限制(Linux 3.2.0 为例)
任一信号量最大值 32767
任一信号量的最大退出时的调整值 32767
系统中信号量集的最大数量 128
系统中信号量的最大数量 32000
每个信号量集的最大信号量 250
5.6.2 创建或打开信号灯集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
功能:创建或者打开信号灯
参数:key: ftok的返回值
nsems:要创建的信号灯包含信号量的个数
0 代表不创建,打开已有的
semflg: IPC_CREAT:创建信号量
IPC_EXCL:如果信号量存在,加了创建参数,报错
返回值:成功返回信号灯id,失败-1
5.6.3 信号灯集初始化
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
功能:控制信号灯
参数:semid 信号灯的id,,信号灯的编号从0开始
cmd:IPC_STAT 获取属性
IPC_SET 设置属性
IPC_RMID 删除
SETVAL 此时需要第四个参数,表示对信号灯的初始化
GETVAL 获取信号灯的值
返回值:失败-1
5.6.4 申请资源和释放资源(pv操作)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
功能:pv操作(v释放,p申请)
参数:semid 信号灯的id
sops
struct sembuf
{
unsigned short sem_num; /* 信号量编号,使用单个信号量时,通常取值为0 */
short sem_op; /* 信号量操作:取值为-1时表示P操作,取值为+1时表示V操作 */
short sem_flg; /* 通常设置为SEM_UNDO,这样进程没有释放信号量时退出时,系统自动释放进程中未释放到的信号量 */
}
nsops 表示执行一次,要操作的信号灯的个数
返回值:成功0,失败-1
5.6.5 demo
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
#if 0
struct sembuf
{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
#endif
int main(int argc, const char *argv[])
{
key_t key;
int semid;
union semun mysemun;
key = ftok(".",'a');
if(key == -1)
{
perror("fail to ftok");
exit(1);
}
semid = semget(key,2,IPC_CREAT|IPC_EXCL|0666);
if(semid == -1)
{
if(errno == EEXIST)
{
semid = semget(key,2,0666);
}
else
{
perror("fail to semget");
exit(1);
}
}
mysemun.val = 10;
semctl(semid,0,SETVAL,mysemun);
mysemun.val = 20;
semctl(semid,1,SETVAL,mysemun);
printf("sem0 val = %d\n",semctl(semid,0,GETVAL));
struct sembuf mysembuf[2];
mysembuf[0].sem_num = 0;
mysembuf[0].sem_op = -5;
mysembuf[0].sem_flg = 0;
mysembuf[1].sem_num = 1;
mysembuf[1].sem_op = 10;
mysembuf[1].sem_flg = 0;
semop(semid,mysembuf,2);
printf("sem0 val = %d\n",semctl(semid,0,GETVAL));
printf("sem1 val = %d\n",semctl(semid,1,GETVAL));
semctl(semid,0,IPC_RMID);
return 0;
}
5.7 共享内存
共享内存允许两个或多个进程共享一个给定的存储区,这样数据就不用在两个进程间复制,所以是最快的IPC,
5.7.1 影响共享内存的系统限制(Linux 3.2.0 为例)
共享内存段最大字节长度 32768
共享内存段最小字节长度 1
系统中共享内存段的最大段数 4096
每个进程共享内存段的最大段数 4096
5.7.2 创建或者打开共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
功能:创建或者打开共享内存
参数:key: ftok的返回值
size: 创建共享内存的大小,如果打开已有的共享内存,可以用0,
shmflg:IPC_CREAT
IPC_EXCL 同消息队列一样
返回值:成功返回共享内存id,失败返回-1
5.7.3 共享内存的映射
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:共享内存的映射
参数:shmid: 共享内存的id
shmaddr: 一般用NULL,表示映射到进程的地址,系统自动选择
shmflg: 0 可读可写
HM_RDONLY 只读
返回值:成功映射完的地址,失败(void *)-1
5.7.4 撤销映射
int shmdt(const void *shmaddr);
功能:取消映射
参数:共享内存的映射地址
返回值:成功0,失败-1
5.7.5 删除共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:控制共享内存
参数:cmd : IPC_STAT 表示获取共享内存的属性,获取到的内容存放在第三个参数
PC_SET 表示设置共享内存的属性,设置的内容在第三个参数存储
IPC_RMID 删除共享内存,第三个传NULL
SHM_LOCK 对该共享内存加锁,只有超级用户可执行
SHM_UNLOCK 解锁
buf 一般为NULL
返回值:成功0,失败-1
注意:多进程操作的时候,不要多次删除同一个共享内存
5.7.6 demo
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
int main(int argc, const char *argv[])
{
key_t key;
int shmid;
void *p = NULL;
key = ftok("./1-homework.c",'a');
if(key == -1)
{
perror("fail to ftok");
exit(1);
}
shmid = shmget(key,512,IPC_CREAT|IPC_EXCL|0666);
if(shmid == -1)
{
if(errno == EEXIST)
{
shmid = shmget(key,0,0666);
}
else
{
perror("fail to shmget");
exit(1);
}
}
p = shmat(shmid,NULL,0);
strcpy(p,"hello world!");
printf("%s\n",(char *)p);
shmdt(p);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
5.8 将文件或设备映射到共享内存
mmap与shmat最大区别就是mmap映射存储段与文件相关联
#include <sys/mman.h>
void *mmap(void *addr,size_t len,int prot,int flags,int fd,off_t offset);
功能:将文件或设备空间映射到共享内存区,因此当从共享内存读数据时就相当于从文件中读取数据
参数: addr:要映射的起始地址,通常为NULL,让内核自动分配
len:映射到进程地址空间的字节数
port:映射区保护方式 PROT_READ 页面可读
PROT_WRITE 页面可写
PROC_EXEC 页面可执行
PROC_NONE 页面不可访问
flags: MAP_SHARED 变动是共享的
MAP_PRIVATE 变动是私有的
MAP_FIXED 准确解释addr参数, 如果不指定该参数, 则会以4K大小的内存进行对齐
MAP_ANONYMOUS 建立匿名映射区, 不涉及文件
fd: 文件描述符
offset:从文件头开始偏移量
p=(STU*)mmap(NULL,sizeof(STU)*5,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0) //STU* 数据类型
int munmap(void *addr,size_t len);
功能:取消mmap函数建立的映射
参数: addr:映射的内存起始地址
len:映射到进程地址空间的字节数
返回值:成功返回0,失败返回-1。
int msync(void *addr,size_t len,int flags);
功能:对映射的共享内存执行同步操作
参数: addr:内存起始地址
len:长度
flags:选项 MS_ASYNC 执行异步写
MS_SYNC 执行同步写
MS_INVALIDATE 使高速缓存的数据失效
返回值:成功返回0,失败返回-1。
六、经典进程同步问题
6.1 生产者-消费者问题
生产者-消费者问题是一个著名的进程同步问题,它描述的是:有一群生产者进程在生成产品,并将这些产品提供给消费者进程去消费,为了使生产者和消费者并发执行,
两者之间设置了一个具有n个缓冲区的缓冲池,生产者将产品放进缓冲区中,消费者从一个缓冲区取走产品,尽管他们是异步进行的,但是需要保持同步,即不允许消费者到空
缓冲区取,不允许生产者向满缓冲区放。
假定生产者和消费者有一个n大小的缓冲区,可以利用锁和信号量来实现进程对缓冲区的互斥和同步,
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define N 4
sem_t sem_X;
sem_t sem_S;
pthread_mutex_t mutex;
volatile int size = 0;
void handler_S(void)
{
while(size < 4){
sem_wait(&sem_S);
pthread_mutex_lock(&mutex);
size++;
printf("size = %d\n",size);
sem_post(&sem_X);
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_exit((void *)0);
}
void handler_X(void)
{
while(size > 0) {
sem_wait(&sem_X);
pthread_mutex_lock(&mutex);
size--;
printf("size = %d\n",size);
sem_post(&sem_S);
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_exit((void *)0);
}
int main()
{
pthread_t pthread_S;
pthread_t pthread_X1;
pthread_t pthread_X2;
sem_init(&sem_S,0,1);
sem_init(&sem_X,0,0);
pthread_mutex_init(&mutex,NULL);
pthread_create(&pthread_S,NULL,handler_S,NULL);
pthread_create(&pthread_X1,NULL,handler_X,NULL);
sem_destroy(&sem_X);
sem_destroy(&sem_S);
pthread_mutex_destroy(&mutex);
pthread_join(pthread_S,NULL);
pthread_join(pthread_X1,NULL);
return 0;
}
6.2 哲学家进餐问题
经典的进程同步问题,描述的是五个哲学家公用一张圆桌,分别坐五张椅子,在餐桌上有五只碗和五只筷,他们生活方式是思考和进餐,平时,一个哲学家
进行思考,饥饿时候拿起左右的筷子进餐,只有拿到两支筷子时才可以进餐,用餐完毕放下碗筷继续思考。
通过问题可以发现筷子是临界资源,一段时间只可以允许一位哲学家使用,为了实现互斥,可以使用一个信号量表示一只筷子,五只筷子由五个信号量代表,
当哲学家饥饿时,先拿起左边筷子,成功后再拿起右边筷子,用餐完毕,先放下左边再放下右边。但是当五个哲学家同时饥饿时,他们同时拿起左边的筷子,但是都拿不到右边的
筷子,这时候都在等待就产生了死锁问题,可以采用以下方式解决:
a.最多只允许四位哲学家拿起左边的筷子
b.当哲学家左右筷子都可用时,才允许他拿起筷子用餐
c.规定奇数号哲学家先拿左边筷子,然后再拿右边筷子,偶数号哲学家相反。