Linux多进程开发
一、进程概述
程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程
- 二进制格式标志:内核利用此信息来解释文件中的其他信息。ELF可执行链接格式
- 机器语言指令:对程序算法进行编码
- 程序入口地址
- 数据:变量的初始值 和 字面量值(如字符串)
- 符号表及重定位表:描述程序中函数和变量的位置及名称。用于调试和运行时的符号解析(动态链接)等。
- 共享库和动态链接信息
- 其他信息:如,如何创建进程
进程是正在运行的程序的实例。在操作系统中,进程既是基本的分配单元,也是基本的执行单元。
从内核的角度看,进程分为用户内存空间和内核数据结构。内核中记录了:进程的标识号IDs,虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录等。
进程的状态转换:就绪态,阻塞态,运行态,(新建,结束)
并发和并行
并行(parallel):在同一时刻,有多条指令在多个处理器上同时执行
并发(concurrency):同一时刻只能有一条指令执行。一般是通过时间片轮转的方式,达到宏观上具有多个进程同时执行的效果。
进程控制块(PCB)
Linux内核的进程控制块是task_struct 结构体
- 进程id:pid_t, 范围:0~32767。ppid 父进程id;pgid 进程组id
- 进程的状态:就绪,运行,阻塞,停止等
- 进程切换时需要保护和恢复的cpu寄存器
- 虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录
- umask掩码:用户权限
- 文件描述符表
- 信号表
- 用户id 和 组 id
- 会话(Session)和进程组
- 进程可以使用的资源上限(Resource Limit)
进程相关命令
查看进程
ps aux / ajx
a:all的缩写,显示终端上的所有进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:显示与作业控制相关的信息
STAT参数意义
- D:不可中断
- R:running
- S :休眠
- T :追踪
- Z :僵尸
- X :死掉的进程
- < :高优先级
- N :低优先级
- s :包含子进程
- + :前台进程
top:显示实时进程动态;
-d 指定显示信息更新的时间间隔,
在 top 命令 执行后,可以按以下按键对显示的结果进行排序:
? M 根据内存使用量排序
? P 根据 CPU 占有率排序
? T 根据进程运行时间长短排序
? U 根据用户名来筛选进程
? K 输入指定的 PID 杀死进程
kill -9 pid :杀死进程
二、进程创建
API
#includepid_t fork(void); 函数的作用:用于创建子进程。 返回值: fork() 在父进程中返回创建的子进程的ID, 在子进程中返回0 通过fork的返回值,可以区分父进程和子进程 在父进程中返回-1,表示创建子进程失败,并且设置error 失败原因:进程号不足;系统内存不足
父子进程之间的关系 区别: 1.fork()函数的返回值不同 父进程中: >0 返回的子进程的ID 子进程中: =0 2.pcb中的某些数据 当前的进程的 pid 当前的进程的父进程的 ppid 信号集
共同点: 由于读时共享,写时复制(拷贝)的特性。在子进程未执行写操作时,以下相同: - 用户区的数据 - 文件描述符表 父子进程对变量是不是共享的? - 变量未修改时,是共享的。修改后,就是不共享的。 - 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。
GDB多进程调试
使用 GDB 调试的时候,GDB 默认只能跟踪一个进程,可以在 fork 函数调用之前,通 过指令设置 GDB 调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程。 设置调试父进程或者子进程:set follow-fork-mode [parent(默认)| child] 设置调试模式:set detach-on-fork [on | off] 默认为 on,表示调试当前进程的时候,其它的进程继续运行,如果为 off,调试当前进 程的时候,其它进程被 GDB 挂起。 查看调试的进程:info inferiors 切换当前调试的进程:inferior id 使进程脱离 GDB 调试:detach inferiors idexec函数族
exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的 内容,换句话说,就是在调用进程内部执行一个可执行文件。
#include
- arg:是执行可执行文件所需要的参数列表 第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称 从第二个参数开始往后,就是程序执行所需要的的参数列表。 参数最后需要以NULL结束(哨兵)
- 返回值: 只有当调用失败,才会有返回值,返回-1,并且设置errno 如果调用成功,没有返回值。 int execlp(const char *file, const char *arg, ... ); - 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。 - 参数: - file:需要执行的可执行文件的文件名 a.out ps
- arg:是执行可执行文件所需要的参数列表 第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称 从第二个参数开始往后,就是程序执行所需要的的参数列表。 参数最后需要以NULL结束(哨兵)
- 返回值: 只有当调用失败,才会有返回值,返回-1,并且设置errno 如果调用成功,没有返回值。
int execv(const char *path, char *const argv[]); argv是需要的参数的一个字符串数组 char * argv[] = {"ps", "aux", NULL}; execv("/bin/ps", argv);
int execve(const char *filename, char *const argv[], char *const envp[]); char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};
三、进程退出、孤儿进程、僵尸进程
孤儿进程(Orphan Process)
定义:父进程运行结束,子进程还在运行
内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。因此孤儿进程并不会有什么危害。
僵尸进程(Zombie)
子进程内核区的pcb信息未释放:若父进程一直运行,未释放该信息,则会形成僵尸进程。
僵尸进程无法被kill -9 杀死
进程回收
父进程可以使用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
区别在于,wait() 函数会阻塞, waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。
一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
API
#include调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行) 如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1. pid_t waitpid(pid_t pid, int *wstatus, int options); 功能:回收子进程,可以不等待(阻塞)。 参数: - pid: > 0 : 某个子进程的pid = 0 : 回收当前进程组的所有子进程 = -1 : 回收所有的子进程,相当于 wait() < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程 - options:设置阻塞或者非阻塞 0 : 阻塞 WNOHANG : 非阻塞 - 返回值: > 0 : 返回子进程的id = 0 : 在非阻塞情况下,表示还有子进程 = -1 :错误 or 没有子进程
退出信息相关宏函数
四、进程间通信
进程是独立的资源分配单位,因此,进程不能直接访问另一个进程的资源。而进程运行时有进行信息交互与状态传递的需求(IPC :Inter Precesses Communication)
目的:数据传输;通知事件;资源共享;进程控制(有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。)
管道
管道是一个在内核内存中维护的缓冲区。可以进行读 / 写操作。匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式操作管道。
管道是字节流,顺序传递,读取顺序与写入顺序一致
管道数据的传递是单向的,一端写入,一端读取;管道是半双工的。
从管道读数据是一次性的,无法使用lseek函数来随机访问数据。
读管道:
- 管道中有数据,read返回实际读到的字节数。
- 管道中无数据:
- 写端被全部关闭,read返回0(相当于读到文件的末尾)
- 写端没有完全关闭,read阻塞等待
写管道:
- 管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
- 管道读端没有全部关闭:
- 管道已满,write阻塞
- 管道没有满,write将数据写入,并返回实际写入的字节数
匿名管道
只能在具有公共祖先的进程间使用。
pipefd【0】是读端,1是写端
有名管道
有名管道提供了一个路径名与之关联,以FIFO的文件行使存在于文件系统中。
内存映射
内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改 内存就能修改磁盘文件。
#includeint munmap(void *addr, size_t length); - 功能:释放内存映射 - 参数: - addr : 要释放的内存的首地址 - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
常见问题
1.mmap什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot
- 只指定了写权限
- prot PROT_READ | PROT_WRITE
第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
2.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()
3.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("XXX");
mmap(,,,,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响。
4.对ptr越界操作会怎样?
越界操作操作的是非法的内存 -> 段错误
信号
信号是事件发生时对进程的通知机制,也称为软件中断,是一种异步通信的方式。(同步需要等待,异步不需要等待)
特点:简单;信息少;满足特定条件才发送;优先级较高
查看信号列表:kill -l 前31个为常规信号,其余为实时信号
- 2 SIGINT crtl + c时产生 终止进程
- 3 SIGQUIT crtl + \时产生 终止进程
- 9 SIGKILL 终止进程,无法被忽略,处理,阻塞
- 11 SIGSEGV 无效的内存访问(段错误) 终止进程并产生core文件
- 13 SIGPIPE 管道破裂,向没有读端的管道写数据 终止进程
- 17 SIGCHLD 子进程结束(收到SIGSTOP; SIGCONT)时发送给父进程 忽略
- 18 SIGCONT 如果进程已停止,则使其继续运行 继续/忽略
- 19 SIGSTOP 停止进程,无法被忽略,处理,阻塞。 暂停进程
查看信号的详细信息: man 7 signal
信号的物钟默认处理动作
-
- Term 终止进程
- Ign 忽略
- Core 终止进程,并生成core文件
- Stop 暂停
- Cont 继续执行被暂停的进程
信号的状态:产生;未决;递达
sigkill 和 sigstop 无法被捕捉,阻塞或者忽略
#includeint kill(pid_t pid, int sig); - 功能:给任意进程 or 进程组,发送信号 - 参数: - pid : > 0 : 将信号发送给指定的进程 = 0 : 将信号发送给当前的进程组 = -1 : 将信号发送给每一个有权限接收这个信号的进程 < -1 : 这个pid=某个进程组的ID取反 (-12345) - sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号 -返回值:成功 0 失败 -1 kill(getppid(), 9); kill(getpid(), 9); int raise(int sig); - 功能:向当前进程发送信号 - 参数: - sig : 要发送的信号 - 返回值: - 成功 0 - 失败 -1 kill(getpid(), sig);
void abort(void); - 功能: 发送SIGABRT信号给当前的进程,杀死当前进程 kill(getpid(), SIGABRT);
定时器
#include信号SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。 alarm(10); -> 返回0 过了1秒 alarm(5); -> 返回9 #include
- new_value: 设置定时器的属性 struct itimerval { // 定时器的结构体 struct timeval it_interval; // 每个阶段的时间,间隔时间 struct timeval it_value; // 延迟多长时间执行定时器 };
struct timeval { // 时间的结构体 time_t tv_sec; // 秒数 suseconds_t tv_usec; // 微秒 }; - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL - 返回值:常规(成功 0 失败 -1 并设置错误号)
信号捕捉函数
#includestruct sigaction { // 函数指针,指向的函数就是信号捕捉到之后的处理函数 void (*sa_handler)(int); // 不常用 void (*sa_sigaction)(int, siginfo_t *, void *); // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。 sigset_t sa_mask; // 使用哪一个信号处理对捕捉到的信号进行处理 // 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction int sa_flags; // 被废弃掉了 void (*sa_restorer)(void); };
信号集
在 PCB 中有两个非常重要的信号集(sigset_t,64位)。一个称之为 “阻塞信号集” ,另一个称之为 “未决信号集” 。这两个信号集都是内核使用位图机制来实现的。 操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数 来对 PCB 中的这两个信号集进行修改。empty:设置为0
fill:设置为1
add:添加
del:删除
ismember:判断是否是成员
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); - 功能:将自定义信号集中的数据设置内核信号集(设置阻塞,解除阻塞,替换) - 参数: - how : 如何对内核阻塞信号集进行处理 SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变 假设内核中默认的阻塞信号集是mask, mask | set SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞 mask &= ~set SIG_SETMASK:覆盖内核中原来的值 - set :已经初始化好的用户自定义的信号集 - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL - 返回值:常规(0, -1) 设置错误号:EFAULT、EINVALint sigpending(sigset_t *set); - 功能:获取内核中的未决信号集 - 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
共享内存
在用户内存空间中,创建共享区,允许多个进程共享,不需要内核参与;因此速度较快。
? 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其 他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
? 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
? 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存, 程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间 中该共享内存段的起点的指针。
? 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存 了。这一步是可选的,并且在进程终止时会自动完成这一步。
? 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之 后内存段才会销毁。只有一个进程需要执行这一步。
共享内存相关的函数 #includeint shmget(key_t key, size_t size, int shmflg); - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。新创建的内存段中的数据都会被初始化为0 - 参数: - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。一般使用16进制表示,非0值 - size: 共享内存的大小 - shmflg: 属性 - 访问权限 - 附加属性:创建/判断共享内存是不是存在 - 创建:IPC_CREAT - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用 IPC_CREAT | IPC_EXCL | 0664 - 返回值: 失败:-1 并设置错误号 成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
void *shmat(int shmid, const void *shmaddr, int shmflg); - 功能:和当前的进程进行关联 - 参数: - shmid : 共享内存的标识(ID),由shmget返回值获取 - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定 - shmflg : 对共享内存的操作 - 读 : SHM_RDONLY, 必须要有读权限 - 读写: 0 - 返回值: 成功:返回共享内存的首(起始)地址。 失败(void *) -1
int shmdt(const void *shmaddr); - 功能:解除当前进程和共享内存的关联 - 参数: shmaddr:共享内存的首地址 - 返回值:成功 0, 失败 -1
int shmctl(int shmid, int cmd, struct shmid_ds *buf); - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。 - 参数: - shmid: 共享内存的ID - cmd : 要做的操作 - IPC_STAT : 获取共享内存的当前的状态 - IPC_SET : 设置共享内存的状态 - IPC_RMID: 标记共享内存被销毁 - buf:需要设置或者获取的共享内存的属性信息 - IPC_STAT : buf存储数据 - IPC_SET : buf中需要初始化数据,设置到内核中 - IPC_RMID : 没有用,NULL
key_t ftok(const char *pathname, int proj_id); - 功能:根据指定的路径名,和int值,生成一个共享内存的key - 参数: - pathname:指定一个存在的路径 /home/nowcoder/Linux/a.txt / - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节 范围 : 0-255 一般指定一个字符 'a'
问题1:操作系统如何知道一块共享内存被多少个进程关联? - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch - shm_nattach 记录了关联的进程个数
问题2:可不可以对共享内存进行多次删除 shmctl - 可以的 - 因为shmctl 标记删除共享内存,不是直接删除 - 什么时候真正删除呢? 当和共享内存关联的进程数为0的时候,就真正被删除 - 当共享内存的key为0的时候,表示共享内存被标记删除了 如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。
共享内存和内存映射的区别 1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外) 2.共享内存效果更高 3.内存 所有的进程操作的是同一块共享内存。 内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。 4.数据安全 - 进程突然退出 共享内存还存在 内存映射区消失 - 运行进程的电脑死机,宕机了 数据存在在共享内存中,没有了 内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
5.生命周期 - 内存映射区:进程退出,内存映射区销毁 - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机 如果一个进程退出,会自动和共享内存进行取消关联。 共享内存操作命令
五、守护进程
控制终端
在UNIX系统中,用户通过中断系统登录后得到一个shell进程,这个终端就是shell进程的控制终端,该信息保存在PCB中。
默认情况下,标准输入、标准输出、标准错误都指向控制终端。
进程组
进程组是一组相关进程的集合, 会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。
进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一 个进程组首进程,该进程是创建该组的进程,其进程 ID 为该进程组的 ID,新进程 会继承其父进程所属的进程组 ID。
会话
一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终 端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程
守护进程
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周 期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
特点:生命周期长,一直运行到系统关闭;在后台运行且不拥有控制终端。
创建步骤