一、基础概念
Linux操作系统一般由以下四个主要部分组成:内核、shell、文件系统和应用程序 。
- 内核(Kernel):是操作系统的核心部分,负责管理系统的硬件资源、进程管理、内存管理、文件系统等。它直接与硬件交互,为其他所有程序提供接口。
- Shell(命令行解释器):是一个程序,用于接收用户输入的命令并执行相应的操作。它提供了一个用户界面,让用户可以与操作系统进行交互。
- 文件系统(File System):是一种组织和管理文件的方法,它将数据存储在磁盘上,并允许用户对这些数据进行访问和操作。
- 应用程序(Applications):是用户使用的软件,如文本编辑器、浏览器、游戏等。
开发应用程序需要开发者编写程序, 程序是包含一系列信息的文件,这些信息描述了如何创建一个进程。进程是程序的实例,可以由一个程序来创建多个进程。
- 在传统操作系统中,进程即是系统资源分配的基本分配单元,也是系统动态执行的基本执行单元。
- 从内核的角度看,进程虚拟地址空间由用户内存空间(程序代码及其使用的变量)和一系列内核数据结构(维护进程状态信息包括进程控制块(PCB)、进程队列、进程调度器等 )组成。
操作系统以时间片(timeslice,操作系统分配给每个正在运行的进程微观上的一段CPU时间,在Linux上大概5ms-800ms)为单位,由内核计算分配给各个用户/进程服务,各个用户/进程可通过终端与计算机进行交互,优先响应一些紧急任务,某些紧急任务不需时间片排队。
- 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
- 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
进程转换状态是进程在其生命周期中可能经历的不同状态。当一个进程从一个状态转换到另一个状态时,它可能需要等待某些条件满足或执行某些操作才能完成状态的转换。
-
新建态:当一个进程被创建时,它处于新建态。此时,它的所有资源尚未分配给它,并且它还没有运行。
-
运行态:一旦进程获得了所需的资源并开始执行,它就进入了运行态。在运行态下,进程正在使用处理器执行代码,并且可能会不断地切换到其他就绪态来等待更合适的时机运行。
-
就绪态:就绪态是指进程已经具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
-
阻塞态:阻塞态是指进程因为某些原因无法继续执行而进入的一种等待状态。常见的阻塞原因包括等待输入/输出操作完成、等待信号量或锁的释放等。在阻塞态下,进程不会占用CPU资源,而是等待相应条件的满足。
-
终止态:终止态是指进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然占用其所分配的资源,直到操作系统将其回收。
为了管理进程,每个进程会分配一个PCB(Processing ControlBlock)进程控制块,维护进程相关的信息。
# struct task_struct 为Linux内核的进程控制块,以下路径可查看其定义(不同版本位置可能有变)
/usr/src/kernels/3.10.0-1160.80.1.el7.x86 64/include/linux/sched.h
# 查看进程可以使用的资源上限
ulimit -a:
# 查看进程信息
ps aux / ajx
a:显示终端上的所有进程,包括其他用户的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:列出与作业控制相关的信息
# 实时显示进程动态
top
# 列出所有信号
kill -l
# 停止指定pid的进程
kill -9 pid
# 停止指定name的进程
kill -9 name
每个进程都由进程号来标识,其类型为pid_t(整型),进程号的范围:0~32767。进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号。
进程号和进程组相关函数:
pid t getpid(void);
pid t getppid(void) ;
pid_t getpgid(pid_t pid);
二、进程创建
/*
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
函数的作用:用于创建子进程。
返回值:
fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。
在父进程中返回创建的子进程的ID,
在子进程中返回0
如何区分父进程和子进程:通过fork的返回值。
在父进程中返回-1,表示创建子进程失败,并且设置errno
父子进程之间的关系:
区别:
1.fork()函数的返回值不同
父进程中: >0 返回的子进程的ID
子进程中: =0
2.pcb中的一些数据
当前的进程的id pid
当前的进程的父进程的id ppid
信号集
共同点:
某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
- 用户区的数据
- 文件描述符表
父子进程对变量是不是共享的?
- 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。
- 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。
*/
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int num = 10;
// 创建子进程
pid_t pid = fork();
// 判断是父进程还是子进程
if(pid > 0) {
// printf("pid : %d\n", pid);
// 如果大于0,返回的是创建的子进程的进程号,当前是父进程
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
printf("parent num : %d\n", num);
num += 10;
printf("parent num += 10 : %d\n", num);
} else if(pid == 0) {
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
printf("child num : %d\n", num);
num += 100;
printf("child num += 100 : %d\n", num);
}
// for循环
for(int i = 0; i < 3; i++) {
printf("i : %d , pid : %d\n", i , getpid());
sleep(1);
}
return 0;
}
/*
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术。
内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。
只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。
也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,
fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
*/
exec函数族(一系列功能相似的函数,类似函数重载的函数)
根据指定文件路径找到可执行文件,并用其取代调用进程的内容。
执行成功不会返回,因为其调用进程除了进程ID等表面信息保持原样,其余如代码段、数据栈和堆栈等都被新内容取代,失败返回-1,原程序调用点继续向下执行。
一般fork后,在子进程中使用该函数
/*
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
- 参数:
- path:需要指定的执行的文件的路径或者名称
a.out /home/nowcoder/a.out 推荐使用绝对路径
./a.out hello world
- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。
*/
#include <unistd.h>
#include <stdio.h>
int main() {
// 创建一个子进程,在子进程中执行exec函数族中的函数
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n",getpid());
sleep(1);
}else if(pid == 0) {
// 子进程
// execl("hello","hello",NULL);
execl("/bin/ps", "ps", "aux", NULL);
perror("execl");
printf("i am child process, pid : %d\n", getpid());
}
for(int i = 0; i < 3; i++) {
printf("i = %d, pid = %d\n", i, getpid());
}
return 0;
}
三、进程退出
孤儿进程指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作 。
僵尸进程指一个子进程在其父进程还没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。当父进程结束时,它的子进程成为了“孤儿进程”,因为它们没有其他进程等待它们结束。这些孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作 。
https://blog.csdn.net/u014530704/article/details/73848573
用fork创建新进程后,常在新进程中调用exec族函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
进程退出、孤儿进程、僵尸进程
常用c语言库的exit函数退出
孤儿进程就是父进程创建的子进程,但是父进程挂了,子进程没挂,被init领养着;
僵尸进程就是子进程执行完了,但是父进程还在运行,但是没wait它,所以没法被kill杀死的进程
四、进程间通信(重点)
匿名管道
管道的特点
为什么使用管道可以进行进程间通信?
因为子进程和父进程在内核中的文件描述符表中的文件描述符指向相同的文件
管道的数据结构:
/*
实现 ps aux | grep xxx 父子进程间通信
子进程: ps aux, 子进程结束后,将数据发送给父进程
父进程:获取到数据,过滤
pipe()
execlp()
子进程将标准输出 stdout_fileno 重定向到管道的写端。 dup2
*/
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
int main() {
// 创建一个管道
int fd[2];
int ret = pipe(fd);
if(ret == -1) {
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
// 关闭写端
close(fd[1]);
// 从管道中读取
char buf[1024] = {0};
/*
设置管道非阻塞
int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(fd[0], F_SETFL, flags); // 设置新的flag
while(1) {
int len = read(pipefd[0], buf, sizeof(buf));
printf("len : %d\n", len);
printf("parent recv : %s, pid : %d\n", buf, getpid());
memset(buf, 0, 1024);
sleep(1);
}
*/
int len = -1;
while((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) {
// 过滤数据输出
printf("%s", buf);
memset(buf, 0, 1024);
}
wait(NULL);
} else if(pid == 0) {
// 子进程
// 关闭读端
close(fd[0]);
// 文件描述符的重定向 stdout_fileno -> fd[1]
dup2(fd[1], STDOUT_FILENO);
// 执行 ps aux
execlp("ps", "ps", "aux", NULL);
perror("execlp");
exit(0);
} else {
perror("fork");
exit(0);
}
return 0;
}
管道的读写特点:
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端
读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程
也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,
再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程
向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程
也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,
直到管道中有空位置才能再次写入数据并返回。
总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
用于有父子关系或者兄弟关系等亲缘关系的进程间的通信
pipe函数
有名管道
/*
创建fifo文件
1.通过命令: mkfifo 名字
2.通过函数:int mkfifo(const char *pathname, mode_t mode);
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname: 管道名称的路径
- mode: 文件的权限 和 open 的 mode 是一样的
是一个八进制的数
返回值:成功返回0,失败返回-1,并设置错误号
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
// 判断文件是否存在
int ret = access("fifo1", F_OK);
if(ret == -1) {
printf("管道不存在,创建管道\n");
ret = mkfifo("fifo1", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
return 0;
}
内存映射
/*
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
- void *addr: NULL, 由内核指定
- length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
获取文件的长度:stat lseek
- 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参数的值一样。
*/
/*
使用内存映射实现进程间通信:
1.有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区
2.没有关系的进程间通信
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 使用内存映射区通信
注意:内存映射区通信,是非阻塞。
*/
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
// 作业:使用内存映射实现没有关系的进程间的通信。
int main() {
// 1.打开一个文件
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END); // 获取文件的大小
// 2.创建内存映射区
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}
// 3.创建子进程
pid_t pid = fork();
if(pid > 0) {
wait(NULL);
// 父进程
char buf[64];
strcpy(buf, (char *)ptr);
printf("read data : %s\n", buf);
}else if(pid == 0){
// 子进程
strcpy((char *)ptr, "nihao a, son!!!");
}
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
内存映射的注意事项:
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会怎样?
偏移量必须是4K的整数倍,返回MAP_FAILED
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被关闭,没有任何影响。
7.对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,);
4K
越界操作操作的是非法的内存 -> 段错误
信号
信号是事件发生时对进程的通信机制,也被称为软件中断,是一种软件层次对中断机制的一种模拟,是一种异步通信的方式。
比如,
- 对于前台进程,比如xshell中命令终端,ctrl+c就会让内核给进程发送一个中断信号,让当前进程中止
- 系统状态变化,比如alarm定时器到期引起的发送SIGALRM信号
- kill命令,比如调用kill函数
- 硬件发生异常
kill -l # 查看系统定义的信号列表
1-31 # 标准信号(常用的9号)
32、33 # 没有
34-64 # 实时信号(目前没有使用未来可能使用的预定义信号)
内核产生信号、信号未被处理即未决、信号被进程接受并处理即递达
三个数字中间的数字就行,最常见的arm的x86架构的值,其他架构规定的信号不用管;
默认处理动作总Core文件是啥?演示一下,先一个错误程序的代码如下:
#include<stdio.h>
#include<string.h>
int main(){
char * buf;
strcpy(buf, "hello");
return 0;
}
程序访问非法内存,看core文件查看错误信息
ulimit -a # 查看一些系统配置信息
ulimit -c 1024 # 更改生成core文件的大小
gcc core.c -g # 生成可调试文件
./a.out # 运行可执行文件,产生错误文件core
gdb a.out #打开调试
core-file core #打开错误文件
信号相关函数
/*
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
- 参数:
- pid :
> 0 : 将信号发送给指定的进程
= 0 : 将信号发送给当前的进程组
= -1 : 将信号发送给每一个有权限接收这个信号的进程
< -1 : 这个pid=某个进程组的ID取反 (-12345)
- sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
kill(getppid(), 9);
kill(getpid(), 9);
int raise(int sig);
- 功能:给当前进程发送信号
- 参数:
- sig : 要发送的信号
- 返回值:
- 成功 0
- 失败 非0
kill(getpid(), sig);
void abort(void);
- 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
kill(getpid(), SIGABRT);
*/
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if(pid == 0) {
// 子进程
int i = 0;
for(i = 0; i < 5; i++) {
printf("child process\n");
sleep(1);
}
} else if(pid > 0) {
// 父进程
printf("parent process\n");
sleep(2);
printf("kill child process now\n");
kill(pid, SIGINT);
}
return 0;
}
定时器函数alarm
/*
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
函数会给当前的进程发送一个信号:SIGALARM
- 参数:
seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
取消一个定时器,通过alarm(0)。
- 返回值:
- 之前没有定时器,返回0
- 之前有定时器,返回之前的定时器剩余的时间
- SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
alarm(10); -> 返回0
过了1秒
alarm(5); -> 返回9
alarm(100) -> 该函数是不阻塞的
*/
#include <stdio.h>
#include <unistd.h>
int main() {
int seconds = alarm(5);
printf("seconds = %d\n", seconds); // 0
sleep(2);
seconds = alarm(2); // 不阻塞
printf("seconds = %d\n", seconds); // 3
while(1) {
}
return 0;
}
alarm函数在调用该函数的进程上设置一个定时时间,时间到了就发送信号终止该进程,该进程二次调用该函数的话会返回上次定时器的剩余时间并重新开始定时,这意味着我想提前终止这个进程的话alarm(0);
SIG_SKILL 和SIG_SOP不能被捕捉是因为kill是杀死一个进程的,如果能被捕捉,那么一个进程不能被杀死那就很可怕了;
/*
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
- 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
- 参数:
- which : 定时器以什么时间计时
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; // 微秒
};
过10秒后,每个2秒定时一次
- old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
- 返回值:
成功 0
失败 -1 并设置错误号
*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
// 过3秒以后,每隔2秒钟定时一次
int main() {
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
/*
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为
- 参数:
- signum: 要捕捉的信号
- handler: 捕捉到信号要如何处理
- SIG_IGN : 忽略信号
- SIG_DFL : 使用信号默认的行为
- 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
回调函数:
- 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
- 不是程序员调用,而是当信号产生,由内核调用
- 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
- 返回值:
成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
失败,返回SIG_ERR,设置错误号
SIGKILL SIGSTOP不能被捕捉,不能被忽略。
*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}
// 过3秒以后,每隔2秒钟定时一次
int main() {
// 注册信号捕捉
// signal(SIGALRM, SIG_IGN);
// signal(SIGALRM, SIG_DFL);
// void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
signal(SIGALRM, myalarm);
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
信号集
PCB进程控制块,有文件描述符表,pid等
内核实现信号捕捉函数的流程
SIGCHLD信号
通过这个信号可以解决多进程中的僵尸进程问题;
子进程结束时,父进程有责任回收它的资源,而wait是阻塞的,父进程也有自己的事情做,这个时候就可以用这个信号
共享内存
共享内存比内存映射效率更高,是效率最高的ICP技术!
共享内存相关的函数
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
新创建的内存段中的数据都会被初始化为0
- 参数:
- key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
一般使用16进制表示,非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
- 参数:
- pathname:指定一个存在的路径
/home/nowcoder/Linux/a.txt
/
- 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),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。
守护进程
守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。
消息队列
Linux进程间通信的消息队列是通过系统调用来实现的。具体来说,有两个常用的系统调用与消息队列相关:
-
msgget()
:该函数用于创建或获取一个消息队列。如果消息队列不存在,则创建一个新的;如果已经存在,则返回该消息队列的标识符。 -
msgsnd()
:该函数用于向消息队列中发送一条消息。它需要指定消息队列的标识符、要发送的消息以及要发送的消息长度。如果消息队列中没有可用的空间,则会阻塞进程直到有空间可用为止。 -
msgrcv()
:该函数用于从消息队列中接收一条消息。它需要指定消息队列的标识符以及要接收的消息长度。如果消息队列中没有可用的消息,则会阻塞进程直到有消息可用为止。
除了上述三个系统调用之外,还有其他一些与消息队列相关的系统调用,例如msgctl()
、msgctl(SIOC_RMID)
等。这些系统调用的具体用法可以参考相关的手册页(man page)。
信号量、和Socket在后文网络通信和多线程中穿插