程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程:
进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息
。操作系统调度进程有两种方式:抢占式调度和非抢占时调度,其中非抢占式调度只有进程阻塞时,CPU才会处理其他资源。抢占式调度系统会根据某种原则终止当前运行的进程去执行另一个以及就绪的进程。抢占式调度的源泽包括:时间片原则、优先级原则、短作业优先原则。
并发是我们需要研究的。
(Linux使用 Ulimit -a可以查看资源上限)
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
有些系统增加了五态模型:
(用户区的数据会自动释放,但内核区的数据还没释放)
。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。ps aux / ajx
a:显示终端上的所有进程,包括其他用户的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:列出与作业控制相关的信息
执行 ps aux后,会显示以下的信息,具体含义看名字就能看出来接下来会详细讲一下STAT参数(进程运行时的状态):
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
所属用户 进程id 占用资源 终端 状态 开始时间 持续时间 执行的命令(产生的该进程)
STAT参数的含义:
D 不可中断 Uninterruptible(usually IO)
R 正在运行,或在队列中的进程
S(大写) 处于休眠状态
T 停止或被追踪
Z 僵尸进程
W 进入内存交换(从内核2.6开始无效)
X 死掉的进程
< 高优先级
N 低优先级
s 包含子进程
+ 位于前台的进程组
执行 ps ajx后,会显示以下的信息(TPGID为-1表示是守护进程
)
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
父进程id 进程id 进程组id 会话id 终端 前台进程组id
执行top 命令会动态显示进程信息。可以在使用 top 命令时加上 -d 来指定显示信息更新的时间间隔,在 top 命令执行后,可以按以下按键对显示的结果进行排序:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
M 根据内存使用量排序
P 根据 CPU 占有率排序
T 根据进程运行时间长短排序
U 根据用户名来筛选进程
K 输入指定的 PID 杀死进程
kill [-signal] pid
小知识:./a.out & 可以让进程在后台运行,不加 & 默认在前台运行,输入 fg 切换到前台。/a.out >> 文件名 可以重定向程序输出的文件,默认是标准输出(当前终端) 参考这篇博客
NULL
,获得当前进程的进程组号)系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。创建进程的函数为:
pid_t fork(void),该函数是一个Linux的系统调用函数,其头文件包括:
#include
#include
返回值:
失败的两个主要原因:
fork()出的子进程不会执行父进程已经执行过的代码,而是会从 fork()
代码处继续向后执行。
Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。
fork之后父子进程共享文件,fork产生的子进程产生与父进程相同的文件描述符,指向相同的文件表,引用计数增加,共享文件偏移指针(父子进程都可以操作文件,父子进程共享文件偏移的指针,具体表现就是,父进程修改了文件,子进程会在父进程修改之后的地方对文件进行操作)。
父子进程之间的关系:
区别:
1.fork()函数的返回值不同
父进程中: >0 返回的子进程的ID
子进程中: =0
2.pcb中的一些数据
当前的进程的 pid 不一样
当前的进程的父进程的 ppid 不一样
信号集
共同点:
某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
- 用户区的数据
- 文件描述符表
父子进程对变量是不是共享的?
- 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。
- 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。
使用 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 id
函数族类似C++的函数重载,函数族是一族函数,可以执行相似的功能。exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代(着重讲是替换!替换!替换!可以理解为用户区内容之间被换了,内核区照旧)
,调用成功从可执行文件的 main 函数处执行,只留下进程 ID 等一些表面上的信息仍保持原样。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。这篇文章讲的非常好,我就不说了,我只写一下参数的意义,其他差不多,也可以去牛客网的C++课程听听老师的课。
//C++的库函数
//头文件 #include
int execl(const char *path, const char *arg, .../* (char *) NULL */);
- 参数:
- path:需要指定的执行的文件的路径或者名称
a.out /home/nowcoder/a.out 推荐使用绝对路径
./a.out hello world
- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
- 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
- 参数:
- file:需要执行的可执行文件的文件名
a.out
ps
- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
//Linux的系统调用
int execve(const char *filename, char *const argv[], char *const envp[]);
exec函数族都以exec作前缀,那么后缀的意义是下面的解释,方便记忆:
l(list) 参数地址列表,以空指针结尾
v(vector) 存有各参数地址的指针数组的地址
p(path) 按 PATH 环境变量指定的目录搜索可执行文件
e(environment) 存有环境变量字符串地址的指针数组的地址
一共有两个函数,exit 和 _exit(该函数是系统调用)
,子进程退出时,子进程只能回收用户区的资源,内核区的资源需要父进程回收。
#include //标准C库的函数
void exit(int status);
#include //Linux系统调用的函数
void _exit(int status);
status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
exit() 函数的执行流程:
进程运行-> exit() -> 刷新I/O缓冲,关闭文件描述符 -> 调用_exit() -> 进程终止
_exit() 函数的执行流程
进程运行 -> 调用_exit() -> 进程终止
exit是系统调用级别的
,它表示了一个进程的结束,它将删除进程使用的内存空间,同时把错误信息返回父进程。return
是语言级别的,表示返回到上一层的调用,即调用堆栈的返回,如果位于main函数中,两个都可以使用,但其实main函数会隐式调用exit().参数 status
获取子进程退出时的信息。父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init (pid为 1 的进程,最最最老的系统进程,Linux系统的第一个进程,其他进程都是他创建的)
,而 init 进程会循环地 wait()
它的子进程(该孤儿进程)。这样,当一个孤儿进程结束生命周期的时候,init 进程就会出面处理它的一切善后工作。所以孤儿进程不会有什么危害。
#include
#include
#include
int main() {
// 创建子进程
pid_t pid = fork();
// 判断是父进程还是子进程
if(pid > 0) {
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
} else if(pid == 0) {
sleep(1);
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
}
// for循环
for(int i = 0; i < 3; i++) {
printf("i : %d , pid : %d\n", i , getpid());
}
return 0;
}
执行课程的代码,会出现下图的情况,原因是当前终端进程(tty)是 ./orphan 进程的父进程,父进程默认在前台执行并输出信息,父进程结束时,当前终端进程回收父进程的资源,并处于阻塞态,但是父进程创建了子进程,该子进程共享父进程的文件描述符信息,包括指向的终端(STDOUT_FILENO,标准输出),所以子进程会把信息输出到当前终端。
僵尸进程不能被 kill -9 杀死
,这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话
,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。避免僵尸进程的方法:
#include
#include
pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,该函数会回收子进程的资源。
参数:int *wstatus
进程退出时的状态信息,传入的是一个int类型的地址,传出参数,函数执行的信息会写入到wstatus中。
返回值:
- 成功:返回被回收的子进程的id
- 失败:-1 (所有的子进程都结束,调用函数失败),两种情况
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.
进程退出信息(wstatus)相关宏函数
#include
#include
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
- pid:
pid > 0 : 某个子进程的pid
pid = 0 : 回收当前进程组的所有子进程
pid = -1 : 回收所有的子进程,相当于 wait() (最常用)
pid < -1 : 某个进程组的组id 等于 pid的绝对值,则回收其中的所有子进程
- options:设置阻塞或者非阻塞
0 : 阻塞
WNOHANG : 非阻塞
- 返回值:
> 0 : 返回子进程的id(当某个子进程被干掉的时候)
= 0 : options=WNOHANG, 表示还有子进程存在(在运行)
= -1 :错误,或者没有子进程了
当创建一个父进程的时候,会默认生成一个进程组,进程组名就是父进程的pid,进程组可以包含多个进程,父进程创建的子进程默认属于父进程的组,也可以指定子进程到别的进程组,如上图所示,pid>0 和 pid = -1 的情况就不做介绍,当pid=0时,会释放父进程所在组的子进程(B、C),指定到其他组的子进程(D、E)不会释放,当 pid < -1时,如果子进程D、E所在的进程组号为2,父进程 A 调用 waitpid(-2) ,因为 |-2|=2,那么进程组2中的 A 的所有子进程都会被释放,B、C进程不会释放。
管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。管道的两端是两个文件描述符(fd[0]、fd[1]),fd[0] 是管道的写出端,fd[1] 是管道的写入端。
例如 统计一个目录中文件的数目命令:ls | wc –l,为了执行该命令,shell 创建了两个进程来分别执行 ls 和 wc。其中ls 和 wc中间的
|
代表管道符
内核内存
中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。
#include
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。
pipefd[0] 对应的是管道的读端
pipefd[1] 对应的是管道的写端
返回值:
成功 0
失败 -1
管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞
注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
通过 fcntl 设置管道非阻塞。
设置管道非阻塞
int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(fd[0], F_SETFL, flags); // 设置新的flag
有名管道和匿名管道的区别:
mkfifo 名字
#include
#include
int mkfifo(const char *pathname, mode_t mode);
#include
#include
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname: 管道名称的路径
- mode: 文件的权限 和 open 的 mode 是一样的,是一个八进制的数(mode & ~umask 是最终权限)
返回值:成功返回0,失败返回-1,并设置错误号
两个代码一个执行有名管道的写操作,一个执行有名管道的读操作。
#include
#include
#include
#include
#include
#include
#include
// 向管道中写数据
int main() {
// 1.判断文件是否存在
int ret = access("test", F_OK);
if(ret == -1) {
printf("管道不存在,创建管道\n");
// 2.创建管道文件
ret = mkfifo("test", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
// 3.以只写的方式打开管道
int fd = open("test", O_WRONLY);
if(fd == -1) {
perror("open");
exit(0);
}
// 写数据
for(int i = 0; i < 100; i++) {
char buf[1024];
sprintf(buf, "hello, %d\n", i);
printf("write data : %s\n", buf);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
#include
#include
#include
#include
#include
#include
// 从管道中读取数据
int main() {
// 1.打开管道文件
int fd = open("test", O_RDONLY);
if(fd == -1) {
perror("open");
exit(0);
}
// 读数据
while(1) {
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
if(len == 0) {
printf("写端断开连接了...\n");
break;
}
printf("recv buf : %s\n", buf);
}
close(fd);
return 0;
}
有名管道的注意事项:
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道
读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据:
管道写端被全部关闭,read返回0,(相当于读到文件末尾)
写端没有全部被关闭,read阻塞等待
写管道:
管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
管道读端没有全部关闭:
管道已经满了,write会阻塞
管道没有满,write将数据写入,并返回实际写入的字节数。
内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。内存映射除了进行进程通信外,还可以进行文件复制等功能。
#include
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
- void *addr: 设为NULL即可, 由内核指定并返回
- length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
获取文件的长度:stat lseek
长度为Linux系统的页的整数倍,如果不够一页,会设为一页的长度,一般一页大小为4KB
- prot : 对申请的内存映射区的操作权限
-PROT_EXEC :可执行的权限
-PROT_READ :读权限
-PROT_WRITE :写权限
-PROT_NONE :没有权限
要操作映射内存,必须要有读的权限。
PROT_READ、PROT_READ|PROT_WRITE
- flags :
- MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
- MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
- fd: 需要映射的那个文件的文件描述符
- 通过open得到,open的是一个磁盘文件
- 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
prot: PROT_READ open:只读/读写
prot: PROT_READ | PROT_WRITE open:读写
- offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不偏移。
- 返回值:返回创建的内存的首地址
失败返回MAP_FAILED,(void *) -1
int munmap(void *addr, size_t length);
- 功能:释放内存映射
- 参数:
- addr : 要释放的内存的首地址
- length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
可以实现匿名映射,即不需要磁盘文件,也不需要文件描述符,这种映射只能实现有关系的进程的通信
。主要区别在于后三个参数,具体含义一看懂。内存映射区的两种通信方式:
- 准备一个大小不是0的磁盘文件
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 使用内存映射区通信
注意:内存映射区通信,是非阻塞。.
内存映射的注意事项:
1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(...);
ptr++; 可以对其进行++操作
munmap(ptr, len); // 错误,要从内存映射的首地址开始释放
2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和prot参数的权限保持一致。
3.如果文件偏移量为1000会怎样?
偏移量必须是4KB的整数倍,返回MAP_FAILED,Linux系统一页的内存默认是4KB,不同系统可能不一样
4.mmap什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot
- 只指定了写权限
- prot PROT_READ | PROT_WRITE
第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
5.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()
6.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("XXX");
mmap(,,,,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响,mmap对fd进行了 copy。
7.对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,,,,,);
内存映射的大小,是一页的大小的整数倍(一页默认为4K)
越界操作操作的是非法的内存 -> 段错误
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
使用信号的两个主要目的是:
信号的特点:
查看系统定义的信号列表:kill -l,前 31 个信号为常规信号,其余为实时信号,查看信号的详细信息:man 7 signal。信号的几种状态:产生、未决、递达
。SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。信号的 5 中默认处理动作:
1、Term 终止进程
2、 Ign 当前进程忽略掉这个信号
3、 core 终止进程,并生成一个Core文件,使用core文件需要首先打开core文件的权限(通过命令ulimit -c 文件大小),然后在编译的时候使用-g
命令,在gdb调试的时候,使用core-file core文件名
即可查看core文件内容。
4、 Stop 暂停当前进程
5、 cont 继续执行当前被暂停的进程
编号 | 信号名称 | 对应事件 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 用户退出shell时,由该shell启动的所有进程将收到这个信号 | 终止进程 |
2 | SIGINT |
当用户按下了 |
终止进程 |
3 | SIGQUIT |
用户按下 |
终止进程 |
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 | SIGALARM |
定时器超时,超时的时间 由系统调用alarm设置 | 终止进程 |
15 | SIGTERM | 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号 | 终止进程 |
16 | SIGSTKFLT | Linux早期版本出现的信号,现仍保留向后兼容 | 终止进程 |
17 | SIGCHLD |
子进程结束时,父进程会收到这个信号 | 忽略这个信号 |
18 | SIGCONT |
如果进程已停止,则使其继续运行 | 继续/忽略 |
19 | SIGSTOP |
停止进程的执行。信号不能被忽略,处理和阻塞 | 终止进程 |
20 | SIGTSTP | 停止终端交互进程的运行。按下 |
暂停进程 |
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的实时信号,它们没有固定的含义(可以由用户自定义) | 终止进程 |
#include
#include
int kill(pid_t pid, int sig);
- 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
- 参数:
- pid :
> 0 : 将信号发送给指定的进程
= 0 : 将信号发送给当前的进程组
= -1 : 将信号发送给每一个有权限接收这个信号的进程
< -1 : 发送给某个进程组,这个进程组号=pid取反 (-12345)
- sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
- 返回值:
- 成功 0
- 失败 非0
int raise(int sig);
- 功能:给当前进程发送信号
- 参数:
- sig : 要发送的信号
- 返回值:
- 成功 0
- 失败 非0
void abort(void);
- 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
kill(getpid(), SIGABRT);
#include
unsigned int alarm(unsigned int seconds);
- 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
函数会给当前的进程发送一个信号:SIGALARM
该函数是非阻塞的
- 参数:
seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
取消一个定时器,通过alarm(0)。
- 返回值:
- 之前没有定时器,返回0
- 之前有定时器,返回之前的定时器剩余的时间
- SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
alarm(10); -> 返回0
过了1秒
alarm(5); -> 上一个定时器失效,并返回9
#include
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
- 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
该函数是非阻塞的
- 参数:
- which : 定时器以什么时间计时
//进程时间 = 用户时间 + 内核时间 + I/O时间
ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM 常用
ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM
ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF
- 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 并设置错误号
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为
- 参数:
- signum: 要捕捉的信号
- handler: 捕捉到信号要如何处理
- SIG_IGN : 忽略信号
- SIG_DFL : 使用信号默认的行为
- 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
回调函数:
- 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
- 不是程序员调用,而是当信号产生,由内核调用
- 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
- 返回值:
成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
失败,返回SIG_ERR,设置错误号
SIGKILL SIGSTOP不能被捕捉,不能被忽略。
产生的信号默认放到未决信号集中,我们只能获取未决信号集,而不能设置;操作的信号集其实只有阻塞信号集
)。信号的处理流程
1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
2.信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
- SIGINT信号状态被存储在第二个标志位上
- 这个标志位的值为0, 说明信号不是未决状态
- 这个标志位的值为1, 说明信号处于未决状态
3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
- 阻塞信号集默认不阻塞任何的信号
- 如果想要阻塞某些信号需要用户调用系统的API
这个标志位的值为0, 说明信号不阻塞
这个标志位的值为1, 说明信号阻塞
4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
- 如果没有阻塞,这个信号就被处理
- 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
以下信号集相关的函数都是对自定义的信号集进行操作。
#include
int sigemptyset(sigset_t *set);
- 功能:清空信号集中的数据,将信号集中的所有的标志位 置为0
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigfillset(sigset_t *set);
- 功能:将信号集中的所有的标志位置为1
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigaddset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigdelset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置不阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigismember(const sigset_t *set, int signum);
- 功能:判断某个信号是否阻塞
- 参数:
- set:需要操作的信号集
- signum:需要判断的那个信号
- 返回值:
1 : signum被阻塞
0 : signum不阻塞
-1 : 失败
系统调用操作PCB中的两个信号集:
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(指向错误地址)、EINVAL(how是非法的)
int sigpending(sigset_t *set);
- 功能:获取内核中的未决信号集
- 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 功能:检查或者改变信号的处理。信号捕捉
- 参数:
- signum : 需要捕捉的信号的编号或者宏值(信号的名称)
- act :捕捉到信号之后的处理动作
- oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
- 返回值:
成功 0
失败 -1
struct 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);
};
SIGCHLD信号产生的条件。
1、子进程终止时
2、子进程接收到 SIGSTOP 信号停止时
3、子进程处在停止态,接受到SIGCONT后唤醒时
以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号,通过该信号可以回收子进程资源,解决僵尸进程代码,仅作参考。
/*
使用SIGCHLD信号解决僵尸进程的问题。
*/
#include
#include
#include
#include
#include
#include
void myFun(int num) {
printf("捕捉到的信号 :%d\n", num);
// 回收子进程PCB的资源
// while(1) {
// wait(NULL);
// }
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret > 0) {
printf("child die , pid = %d\n", ret);
} else if(ret == 0) {
// 说明还有子进程或者
break;
} else if(ret == -1) {
// 没有子进程
break;
}
}
}
int main() {
// 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
// 创建一些子进程
pid_t pid;
for(int i = 0; i < 20; i++) {
pid = fork();
if(pid == 0) {
break;
}
}
if(pid > 0) {
// 父进程
// 捕捉子进程死亡时发送的SIGCHLD信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myFun;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// 注册完信号捕捉以后,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
while(1) {
printf("parent process pid : %d\n", getpid());
sleep(2);
}
} else if( pid == 0) {
// 子进程
printf("child process pid : %d\n", getpid());
}
return 0;
}
共享内存的使用步骤:
#include
#include
int shmget(key_t key, size_t size, int shmflg);
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识,新创建的内存段中的数据都会被初始化为0
- 参数:
- key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
一般使用16进制表示,非0值(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(shmget的一个参数)
- 参数:
- pathname:指定一个存在的路径
- 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),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。
当共享内存的关联进程为0时(最后一个关联的进程也退出了,即使该进程没有调用shmctl函数),
共享内存也会被销毁
共享内存操作命令
ipcs 用法
ipcrm 用法
消息队列就是在内存
中创建一个区域,该形式为链表形式
,支持进程的双向通信。消息队列用一个key进行标识,和共享内存类似,发送的数据类型需要自己定义,而且定义了自己的消息发送和接收函数,在System V 中这两个函数是 msgsnd/msgrcv。具体的API,我不写了,参考下面这两篇博客。
进程间的通讯:消息队列
进程间通信方式
pid_t getpgrp(void);
pid_t getpgid(pid_t pid);
int setpgid(pid_t pid, pid_t pgid);
pid_t getsid(pid_t pid);
pid_t setsid(void);
守护进程创建步骤
守护进程的demo
/*
写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
牛客C++课程总结和复盘。