1.进程相关知识
PCB进程控制块包含的信息
- 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
- 进程的状态,有就绪、运行、挂起、停止等状态。
- 进程切换时需要保存和恢复的一些CPU寄存器。
- 描述虚拟地址空间的信息。
- 描述控制终端的信息。
- 当前工作目录(Current Working Directory)。
- umask掩码。
- 文件描述符表,包含很多指向file结构体的指针。
- 和信号相关的信息(未决信号集、信号屏蔽字)。
- 用户id和组id。
- 会话(Session)和进程组。
- 进程可以使用的资源上限(Resource Limit)
具体更多操作系统相关的知识可以看这里的随笔 <操作系统 - 随笔分类 - imXuan - 博客园 (cnblogs.com)>
2.进程创建
2.1 fork
功能:用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
fork创建子进程,两个进程逻辑上虽然是完全用虚拟内存进行隔离的,但实际上linux引入了读时共享,写时复制的原则,共同读取的数据不需要复制,需要写入的时候再复制,节省空间,具体可以参考操作系统随笔中的内容
#include <sys/types.h> #include <unistd.h> pid_t fork(void); /* 返回值: 成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。 失败:返回-1。 失败的两个主要原因是: 1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。 2)系统内存不足,这时 errno 的值被设置为 ENOMEM。 */
2.2 getpid
功能:获取本进程号(PID)
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); // 返回值:本进程号
2.3 getppid
功能:获取调用此函数的进程的父进程号(PPID)
#include <sys/types.h> #include <unistd.h> pid_t getppid(void); // 返回值:调用此函数的进程的父进程号(PPID)
2.4 getpgid
功能:获取进程组号(PGID)
#include <sys/types.h> #include <unistd.h> pid_t getpgid(pid_t pid); /* 参数:pid:进程号 返回值:参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号 */
2.5 exec 函数族
将当前进程的代码段、数据段等替换成所需要加载程序的代码段、数据段,从新的代码段的第一条指令开始执行,但进程ID不变
exec函数族函数一旦调用成功,不会返回值,只有失败才返回 -1 或 errno
2.5.1 execlp
int execlp(const char* file, const char* arg, ... /* (char*) NULL */); /* 参数 file: 加载程序的名字,需要配合环境变量 PATH 使用 arg0: 可执行文件名 arg1: 参数 ... argn: NULL (哨兵) */
示例
execlp("ls", "ls", "-l", "-F", "-a", NULL);
补充
int main( int argc, char* argv[]) { //函数体内使用了argc或argv …… return 0; } // argv[0]指向程序运行的全路径名 // argv[1]指向在命令行中执行程序名后的第一个字符串 // argv[2]指向执行程序名后的第二个字符串
2.5.2 execl
int execlp(const char* file, const char* arg, ... /* (char*) NULL */); /* 参数 file: 加载程序的绝对路径的程序名字 arg0: 可执行文件名 arg1: 参数 ... argn: NULL (哨兵) */
示例
execl("./bin/ls", "ls", "-l", "-F", "-a", NULL);
3.进程回收
- 父进程有义务在子进程结束时,回收该子进程,隔备进程无回收关系
- 进程终止:
- 关闭所有文件描述符
- 释放用户空间分配的内存
- 进程的 pcb 残留在内核。保存进程结束的状态(正常:退出值。异常:终止其运行的信号编号)
3.1 孤儿进程
父进程先于子进程终止,子进程沦为“孤儿进程”,会被 init 进程领养
ps ajx 指令可以查看进程信息
3.2 僵尸进程(zombie)
子进程终止,父进程未终止,但父进程尚未对子进程进行回收
结束进程指令:kill -9 进程id。只能结束活跃进程,僵尸进程无效,僵尸进程已经结束,只是父进程没有把他干掉,PCB残留在内核中
3.3 wait 回收
- 功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
- 作用
- <阻塞>等待子进程退出(终止)
- 回收子进程残留在内核的pcb
- 获取子进程的退出状态(正常、异常),传出参数:status
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); /* 参数: status : 进程退出时的状态信息。 返回值: 成功:已经结束子进程的进程号 失败: -1 */
示例,通过宏可以获取退出码或者信号编号,也可以传入NULL,不需要保存任何信息,只是把子进程回收
int main(int argc, char *argv[]) { int status = 9; pid_t wpid = 0; pid_t pid = fork(); if(pid == -1) { perror("fork err"); exit(1); } else if(pid == 0) { printf("I'm child pid = %d\n", getpid()); sleep(3); exit(66); } else { wpid = wait(&status); // 保存子进程退出的状态 if(wpid == -1) { perror("wait err"); exit(1); } if(WIFEXITED(status)) // 宏函数为真,说明子进程正常退出 { // 获取退出码 printf("I'm parent, pid = %d child, exit code = %d\n", wpid, WEXITSTATUS(status)); } else if(WIFSIGNALED(status)) // 宏函数为真, 说明子进程被信号终止 { // 获取信号编码 printf("I'm parent, pid = %d child, killed by %d signal\n", wpid, WTERMSIG(status)); } } return 0; }
3.4 waitpid 回收
pid_t waitpid(pid_t pid, int* status, int options); /* pid > 0 等待进程为 pid 的子进程。 pid = 0 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。 pid = -1 等待任一子进程,此时 waitpid 和 wait 作用一样。 pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。 status: 传出回收子进程状态 options: WNOHANG -- 指定回收方式为 “非阻塞”; 0 为阻塞方式 成功返回回收进程的 pid , 失败返回 -1 , 子进程未结束则返回 0 (用非阻塞回收) */
**注意:一次 wait 、 waitpid 调用只能回收一个子进程,想回收 N 个子进程需要将函数放于循环中
4.进程间通信
- 进程间通信的原理,多个进程虽然对应了多个虚拟内存映射,但是系统内核是相同的,可以通过内核传递数据
- 进程间通信的方法
- 1.管道(最简单)
- 2.信号(开销小)
- 3.mmap 映射(速度快,非血缘关系)
- 4.socket 本地套接字(稳定性好)
4.1 pipe(匿名管道)
- 实现原理:Linux 内核使用环形队列机制,借助缓冲器(4k)实现
- 特质
- 本质:伪文件(实际是内核缓冲区)
- 用于进程间通信,由两个文件描述符引用,一个读端,一个写端
- 规定数据从管道写端流入,从读端流出
- 局限性
- 只能自己写,不能自己读
- 管道中的数据,读走就销毁,不能反复读取
- 半双工通信,数据在同一时刻只能在一个方向上流动
- 应用于血缘关系进程间
#include <unistd.h> int pipe(int pipefd[2]); // pipefd : 传入传出参数,其存放了管道的文件描述符 // 管道读端: pipefd[0] // 管道写端: pipefd[1] // 返回值: 成功0; 失败-1, errno
- 示例
int main() { int fd_pipe[2] = { 0 }; pid_t pid; if (pipe(fd_pipe) < 0) // 创建管道 perror("pipe"); pid = fork(); // 创建进程 if (pid == 0) { // 子进程 char buf[] = "I am mike"; write(fd_pipe[1], buf, strlen(buf)); // 往管道写端写数据 _exit(0); } else if (pid > 0) {// 父进程 wait(NULL); // 等待子进程结束,回收其资源 char str[50] = { 0 }; read(fd_pipe[0], str, sizeof(str)); // 从管道里读数据 printf("str=[%s]\n", str); // 打印数据 } return 0; }
管道读写行为
- 读管道
- 有数据:read返回实际读到的字节数
- 无数据:有写端阻塞;无写端返回0(没有相应的read函数)
- 写管道
- 无读端:异常终止(没有相应的write函数)(SIGPIPE信号)
- 有读端:管道满阻塞,管道未满返回实际写入字节数