进程的基本概念:
1、进程与程序
程序是存储在磁盘上的可执行文件,程序被加载到内存中开始运行叫做进程,一个程序可以被多次加载生成多个进程,进程就是处于活跃状态的计算机程序
2、进程的分类
进程根据功能的不同一般分为三种类型:
交互进程:由shell终端启动的进程,在执行过程中需要与用户进程交互操作,可以在前台运行,也可以在后台运行
批处理进程:该进程是一个进程的合集,负责按照顺序启动和执行其他进程
守护进程:一直处于活跃状态,运行在后台,由系统在开机时通过启动脚本boot来开启
3、如何查看进程
简单模式: ps 显示当前用户有终端控制的进程简单信息
列表模式: ps -auxw 显示所有进程的详细信息
a 所有用户有终端控制的进程
x 无终端控制的进程
u 详细信息
w 以更大的列宽显示
USER 属主用户名
PID 进程ID(唯一)
%CPU CPU的使用率
%MEM 内存的使用率
VSZ 虚拟内存使用的字节数
RSS 物理内存使用的字节数
TTY 终端设备号 ?表示无终端控制
STAT 进程的状态
O 就绪态 等待被调用
R 运行态 Linux中没有O,就绪也用R表示
S 可被唤醒的休眠态,例如系统调用,系统中断、等待资源时
T 暂停态,当进程收到停止类信号,会转入暂停态,当收到唤醒信号,会转入运行态
D 不可被唤醒的休眠态,只能被系统唤醒
X 死亡态
Z 僵尸态,进程变成僵尸进程(僵死进程)
s 进程领导者
< 高优先级
N 低优先级
l 多线程的进程
L 有内存页被锁
+ 处于后台的进程
START 启动时间点
TIME 运行时间
COMMAND 启动进程的命令
4、父进程、子进程、孤儿进程、僵尸进程
一个进程可以创建另一个进程,创建者叫做父进程,被创建者叫做子进程,子进程被父进程创建启动后会在操作系统的调度下可以同时运行
孤儿进程:父进程先于子进程结束,子进程变成孤儿进程,被"孤儿院"领养,大部分情况下init(1)是孤儿进程的父进程,由它来回收死亡后的孤儿进程的资源
僵尸进程:子进程先于父进程结束,如果结束时父进程没有回收资源,子进程就变成僵尸进程
5、进程标识符pid
每个进程都有一个非负整数表示的唯一的标识,即进程ID\pid
进程ID在任意时刻都是唯一的,但是可以重用,进程一旦结束后它的ID会被操作系统回收,过一段时间可以重新分配给新建的其他进程使用(延时重用)
getpid
pid_t getpid(void);
功能:获取进程自己的ID
pid_t getppid(void);
功能:获取父进程的ID
注意:init进程PID永远是1
创建进程:
system()函数,底层调用了fork、exec系列函数来创建子进程去执行系统命令程序
pid_t fork(void);
功能:创建一个子进程
返回值:一次调用,两次返回,子进程返回0,父进程返回子进程的ID,当进程的数量超过系统的最大进程数时,则会创建子进程失败,返回-1
1、可以根据返回值的不同让父子进程分别进入不同的分支语句,执行不同的代码
2、通过fork创建的子进程会拷贝父进程的数据段、bss段、堆、栈、I/O流缓冲区等数据,与父进程共享代码段,子进程会继承父进程的信号处理方式
3、通过fork创建的子进程可以共享父进程的文件描述符
[不同进程之间不能共享值相同的文件描述符]
4、通过fork创建的父子进程是独立运行的,因此谁先返回不确定,可以通过睡眠函数,让另一个进程先执行
练习1:制造出僵尸进程、孤儿进程
练习2:父进程创建出四个进程,这四个进程在创建各自创建两个子进程
pid_t vfork(void);
功能:创建子进程,子进程以加载可执行文件的方式运行
返回值:一次调用,两次返回,子进程返回0,父进程返回子进程的ID
区别:
1、子进程一定先返回,但是此时子进程并没有创建成功,需要一个可执行文件来替换当前的所有资源,替换完成,子进程才算创建完成,此时父进程才会返回
2、需要使用exec系列函数让子进程加载可执行文件
3、exec系列函数如果正常执行是不会返回的,当加载可执行文件失败时会返回-1
4、以exec系列函数执行的子进程不会继承父进程的信号处理方式,但是能够继承父进程的信号屏蔽集
5、vfork创建出来的进程在还没有执行exec函数去加载可执行文件程序之前,是共享了父进程的bss、data、stack、heap等内存段,当加载完可执行程序后就会全部替换成可执行程序的内存段,而fork是拷贝父进程的内存段,开始使用的速度比vfork慢
6、vfork创建的子进程在exec函数之前,不应该执行依赖于父进程的操作,否则父进程无法执行操作,导致子进程无法执行exec,形成死锁,死锁形成无法破解,只能避免以上情形
int execl(const char *path, const char *arg, ...);
path:要加载的可执行文件路径
arg:命令行参数,一般第一个是可执行文件名,至少一个,以NULL结尾
例如:execl("./hehe","./hehe","-l","-m",NULL)
int execlp(const char *file, const char *arg, ...);
file:根据可执行文件文件名,会根据PATH环境变量中的路径来加载该文件
arg:同上
int execle(const char *path, const char *arg, ...);
path:要加载的可执行文件路径
arg:同上
envp:环境变量表,把父进程的环境变量表拷贝了一份传给子进程要加载的可执行文件
int execv(const char *path, char *const argv[]);
path:要加载的可执行文件路径
argv:指针数组,命令行参数
int execvp(const char *file, char *const argv[]);
file:根据可执行文件文件名,会根据PATH环境变量中的路径来加载该文件
argv:指针数组,命令行参数
int execvpe(const char *file, char *const argv[],
char *const envp[]);
file:根据可执行文件文件名,会根据PATH环境变量中的路径来加载该文件
argv:指针数组,命令行参数
envp:存储环境变量表的数组
进程的正常退出:
1、在main中执行了return n,该返回值会被父进程接收到
等价于在main中执行了exit(n)
2、进程调用了exit()函数,都会正常结束
void exit(int status);//C标准库函数
功能:在任意时间调用此函数都可以立即结束进程
status:结束状态码EXIT_SUCCESS\EXIT_FAILURE
效果与return中的返回值一样
返回值:不会返回
进程在正常退出之前要完成的事情:
1、先调用事先通过atxeit\on_exit函数注册过的函数
int atexit(void (*function)(void));
功能:注册一个函数在进程结束前执行
int on_exit(void (*function)(int , void *), void *arg);
功能:注册一个函数在进程结束前执行
注意:执行注册函数的顺序与注册顺序相反
2、冲刷并关闭所有的标准I/O流
3、调用_exit/_Exit函数
void _exit(int status);
功能:结束进程,由系统提供
void _Exit(int status);
功能:结束进程,由标准库提供
a、结束状态码会被父进程接收到
b、结束前先关闭所有打开状态下的文件描述符
c、向父进程发送SIGCHLD(17)信号
d、该函数不会返回
3、调用_exit/_Exit函数
4、进程的最后一个线程执行了返回语句
5、进程的最后一个线程调用了pthread_exit函数
进程的异常终止:
1、进程调用了abort函数,产生SIGBRT信号
2、进程接收到某些信号,可能是别的进程发出的,也可能是自己的错误导致发出的
3、进程的最后一个线程收到了"取消"请求操作,并相应该请求
以上三种结束方式父进程都无法获取到异常终止的子进程的结束状态码,因此叫做异常终止
注意:无论进程是如何结束的,它们都会执行同一段代码:关闭所有打开的文件描述符、释放所有的内存
父进程如何回收子进程:
对于任何结束方式,都希望父进程能够知道,通过wait、waitpid函数可以知道子进程是如何结束的以及结束状态码
pid_t wait(int *status);
功能:等待子进程的结束,并获取结束状态码
status:输出型参数
返回值:结束的子进程的PID
1、如果所有的子进程都还在运行,则阻塞
2、有一个子进程结束,立即返回该进程的结束状态码和PID
3、如果没有子进程,则立即返回-1
WIFEXITED(status) 判断子进程是否正常结束,如果是则返回真
WEXITSTATUS(status) 如果进程是正常结束,那么可以获取到正确的结束状态码,只能获取第八位(-128~127)
WIFSIGNALED(status) 判断子进程是否异常结束,如果是则返回真
WTERMSIG(status) 如果进程是异常结束,则获取导致异常的信号ID
注意:如果wait函数可能会阻塞,因此不太适合放在子进程的主业务逻辑中调用,因此子进程结束时会向父进程发送SIGCHLD信号,可以把wait在该信号处理函数中调用,这样就不影响父进程的主业务逻辑
pid_t waitpid(pid_t pid, int *status, int options);
功能:可以指定某个、某些子进程
pid:
< -1等待进程组组ID为abs(pid)的进程组中的任意进程结束
-1 等待任意进程结束,与wait一样
0 等待同组的进程结束
> 0 等待该进程结束
status:结束状态,与wait一样
options:
WNOHANG 非阻塞模式,如果没有子进程返回,立即结束不再阻塞等待
WUNTRACED 如果有子进程转入了暂停态,返回该进程的状态码
WCONTINUED 如果有子进程从暂停态转入运行态,返回该进程状态码
WIFSTOPPED(status) 如果接受到子进程是因为转入暂停,则返回真
WSTOPSIG(status) 如果子进程转入了暂停态,获取导致暂停的信号ID
WIFCONTINUED(status)如果接收到的子进程是因为从暂停态转入运行态,返回真
返回值:成功返回子进程pid,失败-1
进程间通信
基本概念:
什么是进程通信?两个或多个进程之间交互数据的过程,因为进程之间数据是相互独立的,为了协同工作必须进行进程间交互数据
进程间通信的分类:
简单的进程间通信:
信号(携带附加数据)、文件、命令行参数,环境变量表
传统的进程间通信:
管道文件(有名管道、匿名管道)
XSI的进程间通信:
共享内存、消息队列、信号量
网络的进程间通信:
套接字技术Socket
传统的进程间通信:管道文件
管道是UNIX系统中最古老的进程间通信方式,古老意味着所有系统都支持,早期的管道文件支持半双工通信,现在有些系统的管道支持全双工
管道是一种特殊的文件,它的数据在文件中是流动的,读取之后就消失了,如果文件中没有数据可读取时,读取操作会阻塞
有名管道:
基于有文件名的管道文件的通信
通信编程模型:
进程A 进程B
创建管道文件 ...
打开文件 打开文件
写数据 读数据
关闭管道 关闭管道
删除管道文件 ...
如何创建有名管道
1、命令:mkfifo filename
2、函数:
int mkfifo(const char *pathname, mode_t mode);
功能:创建一个有名管道文件
pathname:管道文件的路径
mode:文件权限
匿名管道:
注意:只适合通过fork创建的父子进程之间通信
int pipe(int pipefd[2]);
功能:创建一个匿名管道文件
通过参数pipefd,返回该匿名管道文件的读文件描述符和写文件描述符
pipefd:存储读写fd的数组,输出型参数
pipefd[0]用于读 pipefd[1] 用于写
匿名管道的编程模型:
父进程 子进程
创建获取匿名管道 ...
创建子进程 共享一对fd
关闭读操作 关闭写操作
写数据 读数据
关闭写 关闭读
XSI进程间通信:
X/open公司制定用于进程间通信的系统(S)接口(I)规范
XSI进程间通信都需要借助系统内核完成,需要创建内核对象来进行操作,内核对象以整数形式提供给调用者使用,类似于文件描述符\文件指针作为标识符存在,也叫做IPC标识符
文件描述符\文件指针需要唯一的文件名进行建立,IPC标识符需要借助IPC键值(整数),如果想要创建不同的IPC标识符,需要一个独一无二的IPC键值(别人也没用过)
key_t ftok(const char *pathname, int proj_id);
功能:计算出一个IPC键值
pathname:项目路径
proj_id:项目编号
返回值:根据路径和编号计算出一个IPC键值
注意:项目路径必须是有效路径,否则无论两个参数是否相同,计算出来的IPC键值一定相同,那就没有意义,如果是有效路径,只要两个参数出现一个不同,那么就会计算出不相同的IPC键值,就有意义了
共享内存:
基本特点:
两个或多个进程之间共享一块由内核负责维护的内存,该段内存可以与多个不同的进程的虚拟内存建立映射
优点:操作简单,不需要读写磁盘、不需要复制,最快的一种XSI机制
缺点:需要考虑同步访问的问题,一般使用信号解决
int shmget(key_t key, size_t size, int shmflg);
功能:创建/获取共享内存
key:IPC键值
size:共享内存的大小,获取共享内存时此参数无意义就设置为0
shmflg:
IPC_CREAT 创建共享内存,已存在时则获取
IPC_EXCL 共享内存已存在时则返回失败
如果是获取直接赋0
注意:当创建共享内存时,需要额外提供共享该段内存的权限码
例如shmget(xxx,4096,IPC_CREAT|0666)
返回值:IPC标识符是创建/获取得到的共享内存的标识,错误返回-1
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:虚拟内存与共享内存建立映射关系
shmid:IPC标识符
shmaddr:想要映射的虚拟内存的首地址,如果为NULL时,由操作系统自动选择
shmflg:
SHM_RND shmaddr不为NULL时才有效,表示从shmaddr开始取内存页的整数倍进行映射,提高内存的读写效率
SHM_RDONLY 以只读方式映射共享内存
返回值:成功映射后的内存首地址,失败返回0xFFFFFFFF
int shmdt(const void *shmaddr);
功能:取消虚拟内存首地址与共享内存的映射
shmaddr:虚拟内存的首地址
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:删除/控制共享内存
shmid:IPC标识符
cmd:
IPC_STAT 获取共享内存的属性数据 buf输出型参数
IPC_SET 设置共享内存的属性数据 buf输入型参数
IPC_RMID 删除共享内存 buf无意义NULL
struct shmid_ds {
struct ipc_perm shm_perm; //所有者的相关信息
size_t shm_segsz; //共享内存的字节数
time_t shm_atime; //最后映射时间
time_t shm_dtime; //最后取消映射
time_t shm_ctime; //最后修改时间
pid_t shm_cpid; //创建者的进程号
pid_t shm_lpid; //最后取消映射的进程号
shmatt_t shm_nattch; //当前映射的次数
...
};
编程模型:
进程A 进程B
创建共享内存 获取共享内存
映射共享内存 映射共享内存
写数据并通知其他进程 接到通知后读数据
接到通知后读数据 写数据并通知其他进程
取消映射 取消映射
删除共享内存 ...
消息队列:
基础概念:由内核负责维护管理的数据链表,通过消息类型来对应地收发数据
int msgget(key_t key, int msgflg);
功能:创建/获取消息队列
key:IPC键值
msgflg:
IPC_CREAT 创建消息队列,已存在时则获取
IPC_EXCL 消息队列已存在时则返回失败
如果是获取直接赋0
注意:当创建消息队列时,需要额外提供消息队列的权限码
返回值:IPC标识符是创建/获取得到的消息队列的标识,错误返回-1
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:向消息队列发送消息包
msqid:IPC标识符
msgp:消息包结构体首地址
struct msgbuf {
long mtype; //消息类型
char mtext[n]; //数据
};
msgsz:数据的字节数,不包含消息类型的字节数
msgflg:一般写 0
0 阻塞发送
IPC_NOWAIT 当消息队列满时,立即结束并返回
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
功能:从消息队列中接收对应的消息包中的数据
maqid:IPC标识符
msgp:存储读取到的消息包结构体首地址
msgsz:消息包结构体字节数
msgtyp:接收的消息类型
> 0 读取消息类型为msgtyp的消息包
== 0 读取消息队列中的第一条消息
< 0 读取消息类型小于abs(msgtyp)的消息包,如果有多个则读取消息类型最小值的消息包
msgflg:一般写0 阻塞
IPC_NOWAIT 如果没有符合的消息包,立即返回不阻塞
MSG_EXCEPT 如果msgtyp>0,则读取第一个值不等于msgtyp的消息包
MSG_NOERROR 如果不包含此标志,实际消息包的字节数>msgsz时会返回错误并不读取,如果包含,则不报错并只读取前msgtsz个字节数
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:删除/控制消息队列
shmid:IPC标识符
cmd:
IPC_STAT 获取消息队列的属性数据 buf输出型参数
IPC_SET 设置消息队列的属性数据 buf输入型参数
IPC_RMID 删除消息队列 buf无意义NULL
buf:
消息队列属性结构体
编程模型:
进程A 进程B
创建消息队列 获取消息队列
发送消息 接受消息
接受消息 发送消息
删除消息队列 ....
信号量:
基本特点:由内核维护共享给若干个进程的"全局变量",用于记录共享资源的数量,用于限制进程对共享资源的访问
信号量是一种数据操作锁,本身不具备完整的数据通信交换功能而是通过控制其他的通信资源来更好地实现进程间通信
1、如果信号量的值大于0,说明可以使用资源,需要先信号量-1,然后再使用
2、如果信号量的值等于0,说明没有资源可以使用,此时进程就进入了休眠,直到信号量的值大于0时,进程会被唤醒,执行步骤1
3、当资源使用完毕后,先把信号量的值+1,然后内核会唤醒正在休眠的进程
int semget(key_t key, int nsems, int semflg);
功能:创建/获取信号量
key:同上
nsems:信号量的个数,一般写1
semflg:同上
返回值:IPC标识符
int semop(int semid, struct sembuf *sops, size_t nsops);
功能:对信号量的值进行加减操作
semid:IPC标识符
sops:
struct sembuf{
unsigned short sem_num; //信号量数组的下标,一般写0
short sem_op;
1 信号量的值+1
-1 使用信号量,如果值为0,则阻塞休眠
0 等待信号量的值为0
short sem_flg;
IPC_NOWAIT 不阻塞
SEM_UNDO 如果进程终止后没有还信号量,系统会自动还
}
nsops:表示sops指针指向多少个结构体,每个结构体表示对某一个信号量进行加减操作,一般写1
int semctl(int semid, int semnum, int cmd, ...);
功能:删除/控制信号量
semid:IPC标识符
semnum:第几个信号量,下标从0开始
cmd:
IPC_STAT 获取信号量的属性数据
IPC_SET 设置信号量的属性数据
IPC_RMID 删除信号量
GETALL 获取所有信号量的值
GETVAL 通过返回值获取某个信号量的值
SETVAL 设置某个信号量的属性(主要是值)
SETALL 设置所有信号量的值
GETNCNT 获取等待减信号的进程的数量 -1
GETZCNT 获取等待信号量为0的进程量数量 0
union semun {
int val; //用于设置某个信号量的值
struct semid_ds *buf; //用于设置或获取属性
unsigned short *array; //用于批量设置信或获取号量的值
};