1.进程相关知识
- PCB进程控制块包含的信息
- 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
- 进程的状态,有就绪、运行、挂起、停止等状态。
- 进程切换时需要保存和恢复的一些CPU寄存器。
- 描述虚拟地址空间的信息。
- 描述控制终端的信息。
- 当前工作目录(Current Working Directory)。
- umask掩码。
- 文件描述符表,包含很多指向file结构体的指针。
- 和信号相关的信息(未决信号集、信号屏蔽字)。
- 用户id和组id。
- 会话(Session)和进程组。
- 进程可以使用的资源上限(Resource Limit)
具体更多操作系统相关的知识可以看这里的随笔 <操作系统 - 随笔分类 - imXuan - 博客园 (cnblogs.com)>
- 进程组和会话:多个进程组成进程组,多个进程组组成会话(ps ajx 查看 进程组id 和 会话id)
2.进程创建
2.1 fork
功能:用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
fork创建子进程,两个进程逻辑上虽然是完全用虚拟内存进行隔离的,但实际上linux引入了读时共享,写时复制的原则,共同读取的数据不需要复制,需要写入的时候再复制,节省空间,具体可以参考操作系统随笔中的内容
1 2 3 4 5 6 7 8 9 10 11 12
| #include <sys/types.h> #include <unistd.h>
pid_t fork(void);
|
2.2 getpid
功能:获取本进程号(PID)
1 2 3 4 5
| #include <sys/types.h> #include <unistd.h>
pid_t getpid(void);
|
2.3 getppid
功能:获取调用此函数的进程的父进程号(PPID)
1 2 3 4 5
| #include <sys/types.h> #include <unistd.h>
pid_t getppid(void);
|
2.4 getpgid
功能:获取进程组号(PGID)
1 2 3 4 5 6 7 8
| #include <sys/types.h> #include <unistd.h>
pid_t getpgid(pid_t pid);
|
2.5 exec 函数族
将当前进程的代码段、数据段等替换成所需要加载程序的代码段、数据段,从新的代码段的第一条指令开始执行,但进程ID不变
exec函数族函数一旦调用成功,不会返回值,只有失败才返回 -1 或 errno
2.5.1 execlp
1 2 3 4 5 6 7 8 9
| int execlp(const char* file, const char* arg, ... );
|
示例
1
| execlp("ls", "ls", "-l", "-F", "-a", NULL);
|
补充
1 2 3 4 5 6 7 8 9
| int main( int argc, char* argv[]) { …… return 0; }
|
2.5.2 execl
1 2 3 4 5 6 7 8 9
| int execlp(const char* file, const char* arg, ... );
|
示例
1
| 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
1 2 3 4 5 6 7 8 9 10 11
| #include <sys/types.h> #include <sys/wait.h>
pid_t wait(int *status);
|
示例,通过宏可以获取退出码或者信号编号,也可以传入NULL,不需要保存任何信息,只是把子进程回收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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 回收
1 2 3 4 5 6 7 8 9 10 11 12
| pid_t waitpid(pid_t pid, int* status, int options);
|
**注意:一次 wait 、 waitpid 调用只能回收一个子进程,想回收 N 个子进程需要将函数放于循环中
4.进程间通信
- 进程间通信的原理,多个进程虽然对应了多个虚拟内存映射,但是系统内核是相同的,可以通过内核传递数据
- 进程间通信的方法
- 1.管道(最简单)
- 2.信号(开销小)
- 3.mmap 映射(速度快,非血缘关系)
- 4.socket 本地套接字(稳定性好)
4.1 pipe(匿名管道)
- 实现原理:Linux 内核使用环形队列机制,借助缓冲器(4k)实现
- 特质
- 本质:伪文件(实际是内核缓冲区)
- 用于进程间通信,由两个文件描述符引用,一个读端,一个写端
- 规定数据从管道写端流入,从读端流出
- 局限性
- 只能自己写,不能自己读
- 管道中的数据,读走就销毁,不能反复读取
- 半双工通信,数据在同一时刻只能在一个方向上流动
- 应用于血缘关系进程间
1 2 3 4 5 6
| #include <unistd.h> int pipe(int pipefd[2]);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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信号)
- 有读端:管道满阻塞,管道未满返回实际写入字节数
4.2 fifo
- 创建一个FIFO管道,可以使用 open 函数等系统调用打开
1 2 3 4 5 6 7 8
| int mkfifo(const char *pathname, mode_t mode);
|
- 已经创建一个FIFO管道后,举例使用FIFO文件进行读写操作
1 2 3 4 5 6 7 8 9 10 11 12 13
| int fd = open("my_fifo", O_WRONLY);
char send[100] = "Hello Mike"; write(fd, send, strlen(send));
int fd = open("my_fifo", O_RDONLY);
char recv[100] = { 0 };
read(fd, recv, sizeof(recv)); printf("read from my_fifo buf=[%s]\n", recv);
|
4.3 mmap / munmap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
|
1 2 3 4 5 6 7 8 9 10
| #include <sys/mman.h>
int munmap(void *addr, size_t length);
|
- mmap 中有一个 falgs 是 O_ANONYMOUS ,允许建立一个匿名映射,也就是不需要额外传入一个文件描述符来创建映射区,但这种方式没办法在没有血缘关系的进程间通信
4.4 本地套接字
网络套接字函数 Linux TCP/UDP socket 通信和IO多路复用
4.5 shmget
1 2 3 4 5 6 7 8 9 10 11 12 13
| int shmget(key_t key, size_t size, int shmflg);
int shmid = shmget(100, 4096, IPC_CREAT | 0664); int shmid = shmget(100, 0, 0);
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| void* shmat(int shmid, const void* shmaddr, int shmflg);
void* ptr = shmat(shmid, NULL, 0); memcpy(ptr, "shared memory test", len); printf("%s\n", (char*)ptr);
|
1 2 3
| int shmdt(const void* shmaddr);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl(shmid, IPC_RMID, NULL);
|
如果标记了删除共享内存,共享内存的 key_t 会被修改为0, 也就是不能有新的进程再关联该块共享内存, 接下来等到所有关联该块共享内存的进程结束后, 共享内存会被回收
4.6 shm和mmap对比
- shm 共享内存可以直接创建,mmap 内存映射区需要以来磁盘文件
- shm 效率更高
- shm 直接对内存操作
- mmap 映射需要同步磁盘文件(首次仅建立映射,读哪里哪里缺页才从磁盘拷贝到内存,内存内容改变后一段时间会写入到磁盘文件,也可以msync()强制同步到文件
- 内存共享
- shm 所有进程共享一块内存
- mmap 每个进程都会在自己的虚拟地址空间有一块独立的内存,通过磁盘文件映射
- 数据安全性
- 进程突然退出
- shm 在内核中还会存在
- mmap 在进程的虚拟地址空间会直接消失
- 电脑死机的话由于 mmap 一部分关联磁盘文件,还是会保留一些
- 生命周期
- shm 进程退出,共享内存还在,需要手动调用函数 shmctl(shmid, IPC_RMID, NULL); 删除或者重启电脑
- mmap 进程退出,虚拟地址空间销毁,内存映射区也会销毁
5.信号
5.1 信号相关概念
- 未决:产生与递达(处理)之间的状态。该状态主要受阻塞(屏蔽)影响
- 递达:内核产生信号后递送并且成功到达进程。递达的信号会被内核立即处理
- 信号处理方式:
- 执行默认动作
- 忽略(丢弃)
- 捕捉(调用用户指定的函数)
- 阻塞信号集(信号屏蔽字)
- 本质:位图。用来记录信号屏蔽状态
- 该信号集中的信号表示成功被设置屏蔽。再次收到该信号,其处理动作将延后至解除屏蔽。在此期间该信号一直处于未决态
- 未决信号集
- 本质:位图,用来记录信号的处理状态
- 该信号集中的信号表示信号已经产生,但尚未处理
5.2 信号4要素
编号 |
信号 |
对应事件 |
默认动作 |
1 |
SIGHUP |
用户退出shell时,由该shell启动的所有进程将收到这个信号 |
终止进程 |
2 |
SIGINT |
当用户按下了**<Ctrl+C>**组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 |
终止进程 |
3 |
SIGQUIT |
用户按下**<ctrl+>**组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 |
终止进程 |
4 |
SIGILL |
CPU检测到某进程执行了非法指令 |
终止进程并产生core文件 |
5 |
SIGTRAP |
该信号由断点指令或其他 trap指令产生 |
终止进程并产生core文件 |
6 |
SIGABRT |
调用abort函数时产生该信号 |
终止进程并产生core文件 |
7 |
SIGBUS |
非法访问内存地址,包括内存对齐出错 |
终止进程并产生core文件 |
8 |
SIGFPE |
在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 |
终止进程并产生core文件 |
9 |
SIGKILL |
无条件终止进程。本信号不能被忽略,处理和阻塞 |
终止进程,可以杀死任何进程 |
10 |
SIGUSE1 |
用户定义的信号。即程序员可以在程序中定义并使用该信号 |
终止进程 |
11 |
SIGSEGV |
指示进程进行了无效内存访问(段错误) |
终止进程并产生core文件 |
12 |
SIGUSR2 |
另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 |
终止进程 |
13 |
SIGPIPE |
Broken pipe向一个没有读端的管道写数据 |
终止进程 |
14 |
SIGALRM |
定时器超时,超时的时间 由系统调用alarm设置 |
终止进程 |
15 |
SIGTERM |
程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号 |
终止进程 |
16 |
SIGSTKFLT |
Linux早期版本出现的信号,现仍保留向后兼容 |
终止进程 |
17 |
SIGCHLD |
子进程结束时,父进程会收到这个信号 |
忽略这个信号 |
18 |
SIGCONT |
如果进程已停止,则使其继续运行 |
继续/忽略 |
19 |
SIGSTOP |
停止进程的执行。信号不能被忽略,处理和阻塞 |
为终止进程 |
20 |
SIGTSTP |
停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号 |
暂停进程 |
21 |
SIGTTIN |
后台进程读终端控制台 |
暂停进程 |
22 |
SIGTTOU |
该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生 |
暂停进程 |
23 |
SIGURG |
套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 |
忽略该信号 |
24 |
SIGXCPU |
进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程 |
终止进程 |
25 |
SIGXFSZ |
超过文件的最大长度设置 |
终止进程 |
26 |
SIGVTALRM |
虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间 |
终止进程 |
27 |
SGIPROF |
类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间 |
终止进程 |
28 |
SIGWINCH |
窗口变化大小时发出 |
忽略该信号 |
29 |
SIGIO |
此信号向进程指示发出了一个异步IO事件 |
忽略该信号 |
30 |
SIGPWR |
关机 |
终止进程 |
31 |
SIGSYS |
无效的系统调用 |
终止进程并产生core文件 |
34~64 |
SIGRTMIN ~ SIGRTMAX |
LINUX的实时信号,它们没有固定的含义(可以由用户自定义) |
终止进程 |
5.3 kill
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <sys/types.h> #include <signal.h>
int kill(pid_t pid, int sig);
|
5.4 alarm
- 功能:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送 (14)SIGALRM 信号。进程收到该信号,默认动作是终止,也可以单独设置处理函数。每个进程都有且只有唯一的一个定时器。
1 2 3 4 5 6 7 8 9
| #include <unistd.h>
unsigned int alarm(unsigned int seconds);
|
5.5 setitimer
- 功能:设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
|
1 2 3 4 5 6 7 8 9 10
| struct itimerval { struct timerval it_interval; struct timerval it_value; }; struct timeval { long tv_sec; long tv_usec; }
|
1
| signal(SIGALRM, myfunc);
|
5.6 信号集操作函数
- 未决信号集上的位置为1时,内核会递达对应的动作并进行处理,但可以设置阻塞信号集为 1 ,组织内核处理未决信号集,直到阻塞信号设为 0 后内核再进行处理
1 2 3 4 5 6 7 8 9 10
| #include <signal.h>
sigset_t set;
int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo); int sigismember(const sigset_t *set, int signo); int sigpending(sigset_t *set);
|
5.6.1 sigprocmask 修改信号阻塞集
- 功能: 检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
|
5.6.2 signal 信号处理
- 功能: 注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <signal.h>
typedef void(*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
|
5.6.3 sigaction 信号处理
- 功能:检查或修改指定信号的设置(或同时执行这两种操作)
1 2 3 4 5 6 7 8 9 10 11 12
| #include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
|
1 2 3 4 5 6 7
| struct sigaction { void(*sa_handler)(int); void(*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void(*sa_restorer)(void); };
|
- 注意信号捕捉的一些特性
- sa_mask 只是捕捉函数期间生效的信号阻塞集
- sa_flags = 0 只是捕捉函数执行期间自动屏蔽本信号
- 如果 sa_flags=0 ,它会加入到未决信号集,但由于这是一个位图,所以解除屏蔽后只会执行一次
5.6.4 借助信号捕捉回收子进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include <signal.h> #include <stdio.h> #include <unistd.h> #include <sys/wait.h>
void sig_child(int signo) { pid_t wpid; int status; while((wpid = waitpid(-1, &status, 0)) != -1) { printf("--- catch child pid = %d, ret = %d ---\n", wpid, WEXITSTATUS(status)); } }
int main() { pid_t pid; int i;
for(i = 0; i<15; i++) { if((pid = fork())==0) break; }
if(i == 15) { struct sigaction act;
act.sa_handler = sig_child; sigemptyset(&act.sa_mask); act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
printf("i am parent, pid = %d\n", getpid()); while(1); } else { printf("i am child, pid = %d\n", getpid()); return i; } return 0; }
|
Author:
mxwu
Permalink:
https://mingxuanwu.com/2023/10/28/202310281942/
License:
Copyright (c) 2023 CC-BY-NC-4.0 LICENSE