Linux多进程开发
进程概述
程序和进程
- 程序是包含一些机器语言、程序入口、数据、共享库和链接库的文件,用来描述如何在运行时候创建一个进程。
- 进程就是正在运行的程序的实例 ,是内核定义的抽象的实体,是操作系统的基本分配单元(占用内存和cpu)、执行单元, 由用户内存空间和一系列内核数据结构组成
- 程序就是文件,占用磁盘,一个程序文件可以创建多个进程
单道、多道程序设计
- 单道,cpu中只有一个程序在运行
- 多道,通过管理程序控制,使得cpu同时穿插运行多个程序,宏观上看同时运行,微观上还是一时刻运行一个,提高cpu利用率。
时间片
- 时间片是操作系统内核为进程调度分配的一段cpu时间,一个cpu上穿插运行着多个进程,每个进程占一定的时间片。
- 时间片也不能随便切,很小的话利用率也不一定高
- cpu其实在一个时刻都只能运行一个指令,只是切片了,穿插着人的层面看来同时运行
并行和并发
- 并行,同一时刻,多条指令在多个处理器上同时运行(两队人同时使用两台咖啡机)
- 并发,cpu其实在一个时刻都只能运行一个指令,只是切片了,穿插着运行,人的层面看来同时运行(两队人交替使用一台咖啡机)
进程控制块(PCB)
- 为了管理每个进程,操作系统内核会给每个进程分配一个PCB,存放着这个进程的一些信息,在内核中是一个结构体,放在内核区中
- 信息包括:进程id(唯一,非负整数)、进程状态、cpu时间、一些寄存器数据、文件描述符表、当前工作路径、描述终端控制的信息、用户id和组id、资源上限等
进程状态
- 三态模型
(1) 就绪态:操作系统为进程分配到除cpu外的各项资源之后的状态
(2)运行态:操作系统为进程分配到cpu资源之后,就进入运行态,如果进程的一个时间片执行完之后就转换成就绪态。
(3)阻塞态:如果一个进程sleep或者等待用户,就是停住了,就是阻塞态,阻塞结束后进入就绪态 - 五态模型:就是在三态模型之外还有新建态、终止态
(1) 新建态:就是进程刚刚创建,还未进入就绪队列,就是新建态
(2) 终止态:进程执行完毕、遇到错误退出、被操作系统终止、被其他有权终止的进程终止掉,就进入终止态 ,进入终止态后,等待操作系统来处理删除进程。
进程相关指令
ps aux 或者 ps ajx
# a:显示终端上的所有进程
# u:显示进程的详细信息
# x:显示没有控制终端的进程
# j:列出与作业控制相关的信息
PID:进程的ID,,整数,范围是0~36767,进程号唯一,终止后会重新分配给其他用
PPID: 父进程的ID
PGID:进程组的ID(几个进程一个组成进程组)
SID:会话的ID(几个进程组组成一个会话)
TTY:进程所处的终端
进程号
- 每个进程都有一个号来标识,唯一,被删除后会分配给其他用
- 任何进程(除init进程)之外,都是由另一个进程创建,叫父进程,终端也是一个进程,终端中运行一个程序,那他就创建了一个进程
- 父进程需要负责回收子进程的资源
- 每个进程结束后都会自己释放用户空间,但是内核区需要父进程来释放
进程创建
- 使用man fork可以查看fork的文档
- 使用:
unistd.h为Linux/Unix系统中内置头文件,包含了许多系统服务的函数原型,例如read函数、write函数和getpid函数等。其作用相当于windows操作系统的"windows.h",是操作系统为用户提供的统一API接口,方便调用系统提供的一些服务。
#include <sys/types.h>
#include <unistd.h>
//pid_t fork(void);
//返回值:
// 成功:子进程中返回0,父进程中返回子进程ID
// 失败:返回-1,失败原因——1.进程数达到上限,2.系统内存不足
int main(){
pid_t pid = fork();//进程从这里开始出现分支
if(pid > 0) printf("这是父进程会执行的代码");
if(pid == 0) printf("这是子进程会执行的代码");
printf("这是父进进程和子进程都会执行的代码");
}
- 父进程创建子进程之后,用户内存空间是拷贝了一份过去的,所以一开始它们拥有的值一样,但后续操作中,两个进程之间互不影响,他们空间是独立的。
- 上面说的拷贝,不是创建完就新开辟一个原样的物理内存空间,这样有浪费,他是写时拷贝
- 写时拷贝,就是刚创建时候,子进程和父进程其实还是映射着同一个物理内存地址的,但一个进程对这个地方进行写操作时候,才会新开辟一个物理内存空间。这样的目的是为了降低内存消耗和拷贝内存的时间
- 创建子进程之后,内核区也是拷贝过去的,里面除了进程管理块(PCB)里面的进程号(pid)东西不一样,其他比如文件描述符那些是一样的
GDB调试多进程
exec函数族
- exec族函数是用来在一个程序中再启动一个可执行程序,取代原来的进程中的内容,用户区的数据被取代
- 如果启动成功,原来的进程已经被取代了,也就没有返回一说了,如果启动失败,返回-1,原进程继续往下执行
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
//path;可执行程序的路径
//arg:可执行程序的参数,参数需要以null结尾(哨兵)
//返回值:只有调用失败的时候才会返回-1,其他没有
- 举例
hello程序,编译生成hello可执行程序
#include <unistd.h>
#include <stdio.h>
int main(){
printf("hello");
}
调用程序
#include <unistd.h>
#include <stdio.h>
int main(){
pid_t pid = fork();
if(pid > 0)
printf("father process");
else{
execl("hello", "hello", NULL);//第一个为路径,后面的为参数,第一个参数得填,其实没什么用,参数需要以NULL结尾
printf("original child process\n");
}
}
- 也可用用来执行linux的一些系统命令,它们其实也是可执行程序,比如ps aux、ls等等,
通过whic ps 就可以查看到ps在哪
execl("/bin/ps","ps","aux",NULL)
- execlp
这个相比于excel,他可以去环境变量里面寻找可执行程序,比如就可以这样用
execlp(("ps","ps","aux",NULL);
进程控制
进程退出
- _exit()是linux的库函数
- exit()是c库函数
#include <stdlib.h>
void exit(int status);//这个是c库的函数,status是设置要返回的退出状态
#include <unistd.h>
void _exit(int status);//这个是linux的函数,status是设置要返回的退出状态
孤儿进程
- 父进程提前结束之后,子进程还在运行,那他就叫孤儿进程
- 出现这样情况时,孤儿进程就交给init() 进程处理,孤儿进程结束之后,它来回收孤儿的资源
- 孤儿进程是没有什么危害的
僵尸进程
- 就是进程结束之后,但是父进程没有进行回收释放,他就占用着空间和进程号,它就是一个僵尸进程。
- 他都不能被kill -9 杀掉,危害很大,可能导致它占着号,占太多满了后面可能就开不了新进程了。
- 如果此时父进程结束,那僵尸进程就会变成孤儿进程
- 解决方式就是在父进程中用wait()或者waitpid()得到他的退出状态并彻底清除这个进程。
- wait()和waitpid()功能差不多,wait()是阻塞的,调用一次处理一个子进程,waitpid是非阻塞的,并且可以指定等待的进进程号
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
//它会等待任意一个子进程结束,任意一个子进程状态变化之后就去执行释放
//参数:传入一个指针,可以获取到子进程状态
//返回值: 1.返回被释放的进程号,2.没有子进程可以释放了或者调用失败返回-1
pid_t waitpid(pid_t pid, int *wstatus, int options);
//参数:
// pid:
// pid > 0: 指定子进程的pid
// pid = 0:回收当前进程组内的所有子进程
// pid = -1:回收所有的子进程,相当于wait()
// pid < -1: 要回收指定进程组的pid的绝对值,回收那个指定进程组中的子进程
// *wstatus: 获取子进程结束的状态
// options: 0-阻塞,WNOHANG-非阻塞
进程间通信(IPC)
基本概念
- 进程之间使用的资源是独立的,不能直接去访问其他进程的资源
- 但是进程不是孤立的,它们可以进行信息和状态的交互
- 进程之间通信的作用:数据传输、通知事件、资源共享、进程控制
4.这里记录一个概念:进程间资源共享的时候,需要同步,这个同步的概念理解是,比如医院看病,一个人看完另一个再去,这叫同步,俩人一起去看病那叫异步。
Linux进程间通信的方式
管道
管道(匿名管道)概述
这上面哪个 | 叫做管道符,他的原理就是在当前终端创建一个管道,连接ls 和 wc 两个进程
- 管道是内核空间 中维护的缓冲区,是有限的,不同操作系统不一样
- 管道拥有文件的特质:读、写操作,管道的两端对应着文件描述符
- 匿名管道没有文件实体,有名管道有文件实体,但是也只是在内存中的,不会在磁盘上有一个实际的文件
- 管道是一个字节流,不需要定义通信协议,可以从管道中读取任意大小的数据
- 管道传递的数据是顺序的,先进先出
- 管道是半双工的,就是同一时刻只能一端发,另外一端收,不能同时刻两边都发都收。
- 从管道读取数据时一次性操作,一旦被读走,就从管道中被抛弃了
- 匿名管道只能在有公共祖先(父进程与子进程、兄弟进程等)的进程间使用
- 由于子进程与父进程之间其实是有一样的文件描述符,所以它们之间可以通过文件交换数据,同时,管道又是有文件的特性,所以管道就可以用于进程之间的通信。
- 同样由于 上面10说的特性,只有有亲缘关系的进程之间它们才满足这样的条件,所以就是如8所说
- 管道的数据结构,是环形队列
使用匿名管道进行进程间通信
子进程和父进程两个都去管道里面读取数据的时候,就是第二种情况,有可能出现自己发的数据自己又去读回来,这样是不对的,所以一般只用第三种情况,一边发一边收,这样比较合适。
-
当管道以阻塞模式运行时候
读管道:
管道中有数据:就返回读到的字节数
管道中无数据:
管道写端都被完全关闭,那就返回0,相当于读到了文件末尾
管道写端没有完全关闭,那就是阻塞,直到有数据进来
写管道:
管道读端被完全关闭:就会收到一个SIGPIPE信号,进程异常终止
管道读端没有被完全关闭:
管道已满:阻塞,直到管道内有空间写入
管道未满:写入数据,并返回实际写入的字节数 -
可以通过fcntl函数设置文件描述符的模式,使他变成非阻塞的,这样管道就是非阻塞的了,当非阻塞的时候,如果没有数据,read返回的就是-1。
有名管道
- 匿名管道只能在有关系的进程之间通信
- 有名管道(FIFO),也叫命名管道、FIFO文件
- 有名管道是以文件形式存在于文件系统中的,在磁盘上能看到文件,但是里面是不存数据的,管道的数据都是在内存中(内核区)的,他的打开方式和打开一个普通文件一样,因此即使无关的的进程也能通过FIFO交换数据
- 对他的操作其实就是跟普通文件、匿名管道操作一样,数据读取顺序也是先入先出,FIFO(就是先入先出的意思)
- 使用FIFO的进程退出后,FIFO文件继续保存在文件系统中以便以后使用
- FIFO是有名字的,不同的进程都可以打开管道进行通信
有名管道的使用
一般也是,管道设置成一端只写,另一端只读,这样比较好
用有名管道实现一个聊天功能
- 实现思路,实现半双工
内存映射
- 内存映射也是一种进程间通信的方式,用内存映射磁盘上面的空间,不同进程映射同一块文件,这样一个进程修改了文件,另一个进程就能相应同步改变
- 用内存映射的方式效率比较高的
mmp内存映射
port的一定要有读权限,才能操作文件,port的权限要小于等于open的权限
munmap释放内存映射
使用内存映射进行进程间通信
- 使用内存映射进行通信,不仅可以在父子进程之间,也可以在不想关的进程之间
使用内存映射的注意事项
信号
信号基本概念
- 信号是linux比较古老的一种通信机制,理解为软件中断,一个进程执行过程中,被另外一个异步的进程中断,转去执行另外一个突发事件。
常见的一些信号
信号使用
kill——给指定进程发送指定信号
rase——给当前进程发送指定信号,abort——给当前进程发送终止信号
alarm函数——设定定时器
该函数时是不阻塞的
4. alarm测试
这里时把一秒内的数字都输出到终端,经过执行发现,终端其实超过了一秒才完全输出完数据
这里的原因时,往终端输出数据是需要花费挺多时间的,并且终端输出的缓冲区也是有限的
这里可以这样操作一下,不直接输出到终端,让他重定向输出的文件中
这样发现速度超级快,并且文件中数据远远比终端输出的要多,当然这样写文件,也是会花费一些时间的
程序执行时间其实是由这几部分组成的
setitimer——设置周期性的定时器
- 具体设置步骤
信号捕捉
- signal-函数
- sigaction函数
他和signal函数都是信号捕捉,这个的功能更多一些,signal是一个特殊标准,更推荐使用sigaction
信号集
信号集操作函数
- 这些函数只能操作自己定义的信号集(就是那个结构体)进行一些设置
调用系统函数使用设置的信号集
- 上面提到的信号集操作,是这样的,定义一个信号集结构体之后(sigset_t set),用上面那些函数,可以对修改结构体里面的值,但是修改之后,他还是不能发挥作用的,要发挥作用,就需要调用系统函数,使设置好的信号集生效。
命令行的一些补充知识
- 中运行程序分为前台运行和后台运行
- 前台运行的时候,他就是会占用这个终端,此时你在终端输入 命令是没有用的
- 后台运行的时候,他会往终端打印信息,但是不会占用终端,你输入命令也是可以用的,通过在运行程序后面加个&号,他就是以后台方式运行
- 当程序在后台运行时,输入fg命令,就可以切换到前台
SIGCHLD信号
共享内存
- 他的步骤,先创建、再关联、再分离、最后再销毁
共享内存操作函数
- shmget——创建共享内存
key参数那里,如果进程1是100,进程2也要设为100,这样他俩才能共享到同一块内存 - shmat——将当前进程与共享内存关联
- shmdt——将共享内存与当前进程分离
当一个进程结束时,会自动和共享内存分离
一个进程和一个共享内存分离后,就不能再操作这块内存了,也不能再次去关联 - shmctl——操作共享内存
设置了IPC_RMID之后,共享内存的key就会变成0,此时只是标记删除,要等所有的进程都解除关联之后,才会被真正释放。
共享内存注意事项
使用共享内存进行进程间通信
-
写进程:
-
读进程
这里读这端,他去获取的共享内存大小不能大于开辟的那边的大小
查看进程间通信方式和共享内存有关的命令
消息队列
在内核中会开辟出一块空间,是一个存放消息的队列(先进先出),本质是个链表,进程通过获取对应的消息队列id,往队列里面发送或者读取消息,
通过ipcs -q可以查看到目前有的消息队列
linux中有两种消息队列
System V
- msggget可以完成消息队列的创建,传入的第一个参数决定着这个消息队列的身份标识,两个进程要想访问同一个消息队列的话,这个值需要一样
- 这个值可以通过一个ftok()函数来创建
- 消息里面的mtype是消息类型,你发什么类型的消息,就要读什么类型的消息
发送消息
消息接收
Posix
发送消息
接收消息
守护进程
终端
进程组
会话
- 进程组的首进程不能创建新的会话
3. 开启一个终端后,就自动启动一个bash进程,在后台运行,他就会自动生成一个进程组,然后再生成一个会话,他的PID是400,PGID也是400,SID也是400
4. 然后启动了find和wc在后台运行,find的PID是658,那find就又生成一个进程组,进程组号PGID是658,wc和find同在一个进程组中,所以wc的PGID是658
5. 然后启动了sort和uniq在后台运行,sort的PID是660,那sort就又生成一个进程组,进程组号PGID是660,sort和uniq同在一个进程组中,所以uniq的PGID是660
6. 由于658、660和400都是会话400下面的进程组,所以这些里面的进程他们的会话SID都是400
7. 由于一个会话智能又一个前台进程组,所以660在前台,660里面的进程才可以使用终端输入输出,658和400都在后台
守护进程
守护进程的创建
- 要用子进程去创建新会话的原因:在调用setsid()的时候会直接创建一个新会话,会话的SID为该进程的PID,组PGID也是该进程的PID,如果用的是父进程来创建新会话,由于父进程PID=100是他所在进程组的首进程,进程组的PGID=100,当父进程调用setsid之后,创建的新会话中就会和目前的冲突,所以不能用父进程来创建新会话。
- 案例
/* 写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。*/
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
void work(int num) {
// 捕捉到信号之后,获取系统时间,写入磁盘文件
time_t tm = time(NULL);
struct tm * loc = localtime(&tm);
// char buf[1024];
// sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
// ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);
// printf("%s\n", buf);
char * str = asctime(loc);
int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
write(fd ,str, strlen(str));
close(fd);
}
int main() {
// 1.创建子进程,退出父进程
pid_t pid = fork();
if(pid > 0)
exit(0);
// 2.将子进程重新创建一个会话
setsid();
// 3.设置掩码
umask(022);
// 4.更改工作目录
chdir("/home/nowcoder/");
// 5. 关闭、重定向文件描述符
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
// 6.业务逻辑
// 捕捉定时信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = work;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
struct itimerval val;
val.it_value.tv_sec = 2;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 2;
val.it_interval.tv_usec = 0;
// 创建定时器
setitimer(ITIMER_REAL, &val, NULL);
// 不让进程结束
while(1)
sleep(10);
return 0;
}