程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程。
信息文件
进程是正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的执行单元,也是基本的分配单元。进程是系统资源分配的最小单位,一个进程可以有多个线程。
进程与内核的关系
可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。
从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。
记录在内核数据结构中的信息包括许多与进程相关的标识号 (IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。
单道程序设计是指在一个时间段内,计算机内存中只允许一个的程序运行,一旦一个程序开始执行,直到它执行完毕或者发生错误,其他程序都无法执行。
多道程序设计是指在一个时间段内,计算机可以同时加载和执行多个程序,使多个程序在管理程序控制下相互穿插运行并在计算机系统中同处于开始到结束之间的状态,这些程序共享计算机系统资源,从而提高系统资源和CPU的利用率。
CPU处理程序
对于一个单 CPU 系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU 上运行的程序只有一个。在多道程序设计模型中,多个进程轮流使用 CPU。而当下常见 CPU 为纳秒级,1秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
时间片 (time slice) 是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。
在只考虑一个 CPU 的情况下,这些进程“看起来像”同时运行的,实则是轮番穿插地运行。由于时间片通常很短(在 Linux 上一般为几ms到几十ms),用户不会感觉到。
时间片在内核中的作用
时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。
并行(parallel)指在同一时刻,有多条指令在多个处理器上同时执行。
并发(concurrency)指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的只是把时间分成若干段,使多个进程快速交替的执行。
理解例子
并行是两个队列同时使用两台咖啡机:
并发是两个队列交替使用一台咖啡机:
为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个 PCB(Processing Control Block)进程控制块,维护进程相关的信息 Linux 内核的进程控制块是 task_struct
结构体。
在以下文件中可以查看 task_struct
结构体定义:
/usr/src/linux-headers-xxx/include/linux/sched.h
需要了解的 task_struct
结构体内容
查看资源上限的命令
ulimit -a
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。
在三态模型中进程状态分为三个基本状态:
在五态模型中,进程状态在三个基本状态上添加了新建态、终止态
a
- 显示终端上的所有进程,包括其他用户的进程
u
- 显示进程的详细信息
x
- 显示没有控制终端的进程
j
- 列出与作业控制相关的信息
ps aux
ps ajx
STAT参数含义
top
可以在使用 top
命令时加上 -d n
来指定显示信息更新的时间间隔
top -d 5
更新排序
进入top后界面键入以下键盘按键可按规则显示
M
:根据内存使用量排序P
:根据 CPU 占有率排序T
:根据进程运行时间长短排序U
:根据用户名来筛选进程K
:输入指定的PID杀死进程Q
:退出top界面语法:kill 信号选项 pid
列出所有信号
kill -l
# 强制终止进程的命令
kill -9 pid #kill -SIGKILL pid
# 建议先向进程发送一个终止请求,允许进程执行清理工作并正常退出(15是kill默认信号)。别直接一来就9,除非出现严重问题。
kill -15 pid #kill -SIGTERM pid
根据进程名杀死进程
killall name
每个进程都由 进程号(PID) 来标识,其类型为 pid_t
(整型)进程号的范围: 0~32767。进程号是唯一的,但可以重复使用。当一个进程终止后,其进程号就可以再次使用。
任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程对应的进程号称为 父进程号 (PPID) 。
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个 进程组号 (PGID) 。默认情况下,当前的进程号会当做当前的进程组号。
pid_t getpid(void);
pid_t getppid(void);
pid_t getpgid(void);
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。
pid_t fork(void);
失败的两个主要原因
代码示例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
// 创建子进程
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());
}
else if (pid == 0)
{
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
}
for (int i = 0; i < 5; i++)
{
printf("i : %d, pid : %d\n", i, getpid());
sleep(2);
}
return 0;
}
ps:在这放一道有争议的题,题目源于七牛云的笔试题,18年的题至
今都还有人讨论答案,欢迎各位大佬前来探讨。
以下程序输出()个 “-”
int main(void) { int i; for (i = 0; i < 2; i++) { fork(); printf("-"); } return 0; }
A. 2 ?????? B. 4 ?????? C. 6 ?????? D. 8
linux环境下GCC编译后运行一共打印8个
将 printf("-");
改为 printf("-\n");
后同环境下打印6个
fork()
时,创建一个子进程fork()
会给父进程返回子进程的 PID ,会给子进程返回 0fork()
之后的代码)sleep()
显式体现cpu交替处理父子进程扩充上一小节第3步。
调用fork()
后,子进程的用户区数据与父进程相同但是 fork()
返回值不同,核区数据也会拷贝过来但是 pid 不同。
代码示例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int num = 10;
// 输出num原定义值
printf("original num: %d\n", num);
// 输出num原地址
printf("Address of original num: %p\n", &num);
// 创建子进程
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);
// 输出父进程中num的地址
printf("Address of num in parent precess: %p\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);
// 输出子进程中num的地址
printf("Address of num in child precess: %p\n", &num);
}
// for (int i = 0; i < 5; i++)
// {
// printf("i : %d, pid : %d\n", i, getpid());
// sleep(2);
// }
return 0;
}
最终打印结果发现两地址进程相同,这是因为我们所理解的是虚拟内存地址,虚拟内存地址每个进程都是共享的,而mmu所映射的物理内存地址是不一样的,通过写操作会拷贝数据到新的物理内存地址。
从操作系统来理解,每个进程有自己的页表,父进程 fork 出新的子进程时,子进程拷贝一份父进程的页表,且父子进程将页表状态修改为写保护。当父进程或子进程发生写操作时将会发生缺页异常,缺页异常处理函数将会为子进程分配新的物理地址。由于不同的进程的页表不同,因此访问同样的逻辑地址对应的物理地址才不同。
当一个进程或线程想要修改共享数据时,会先创建该数据的副本,然后再进行修改。
原始数据保持不变,其他进程或线程仍可读取原始数据的副本。这个策略避免了多个进程同时修改数据时的竞争条件,从而提高了并发性能。
上面的fork()
的实现就是读时共享,写时拷贝,当资源读取的时候内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只有在需要写入的时候才会复制到新的地址空间,从而使各个进行拥有各自的地址空间。
STL标准模板库中的 string
类,就是一个具有写时拷贝技术的类。
通常 string
类中必有一个私有成员,其是一个 char*
,用户记录从堆上分配内存的地址,其在构造时分配内存,在析构时释放内存。
因为是从堆上分配内存,所以 string
类在维护这块内存上是格外小心的,string
类在返回这块内存地址时,只返回 const char*
,也就是只读的,如果需要写,只能通过 string
提供的方法进行数据的改写。
注意:
区别
fork()
的返回值不同
共同
当子进程刚被创建出来,还没有执行任何的写数据的操作,以下对象父子进程共享
父子进程对变量是不是共享的?
刚开始是共享的,一旦修改数据共享不了。读时共享,写时拷贝。
工作一般不用,面试较多。
使用GDB 调试的时候,GDB 默认只能跟踪一个进程,可以在 fork()
调用之前,通过指令设置 GDB 调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程。
代码示例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(){
printf("begin\n");
if (fork()>0)
{
printf("我是父进程: pid = %d, ppid = %d\n", getpid(), getppid());
int i;
for(i = 0; i< 10; i++){
printf("i = %d\n", i);
sleep(1);
}
}else{
printf("我是子进程: pid = %d, ppid = %d\n", getpid(), getppid());
int j;
for(j = 0; j< 10; j++){
printf("j = %d\n", j);
sleep(1);
}
}
return 0;
}
进行GDB调试
# 查看源代码
l
# 设置父进程打印断点
b 9
# 设置子进程打印断点
b 16
# 运行
r
设置调试父进程(默认)
set follow-fork-mode parent
设置调试子进程
set follow-fork-mode child
进行GDB调试
调试当前进程,其他进程继续运行(默认)
set detach-on-fork on
调试当前进程,其他进程被GDB挂起
set detach-on-fork off
进行GDB调试
info inferiors
inferiors id
detach inferiors id
在调用进程内部执行一个可执行文件。
exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段、据段和堆栈等都已经被新的内容取代,只留下进程ID 等一些表面上的信息仍保持原样;只有调用失败了才会返回 -1并从原程序的调用点接着往下执行。
调用exec 函数族并不是新建一个进程而是只替换用户区的数据
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., 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[]);
含义区分
l
:调用程序每个命令行参数需单独写成列表形式并以空指针结束p
:如果函数参数 file
含 /
就视为路径名,否则按 PATH 环境变量指定的目录搜索可执行文件v
:需先构造一个命令行参数的指针数组,然后将数组地址作为调用程序参数e
:需先构造环境字符串指针数组,然后将数组地址传给函数使用新的环境变量代替调用进程的环境变量参数
path
:可执行文件的路径名字
file
:按 PATH 环境变量指定的目录搜索可执行文件
arg
:可执行程序所带的命令行参数,第一个参数为可执行文件的名字(没什么用,一般写这个),从第二个参数开始就是程序所需参数列表,最后必须以NULL结束
argv[]
:命令行参数的指针数组
envp[]
:环境字符串指针数组
返回值
执行成功后不会返回,因为调用进程的实体,包括代码段、据段和堆栈等都已经被新的内容取代,只留下进程ID 等一些表面上的信息仍保持原样;只有调用失败了才会返回 -1并设置erron从原程序的调用点接着往下执行。
代码示例
hello.c
#include <stdio.h>
int main(){
printf("helloworld!\n");
return 0;
}
编译后为hello可执行文件
execl.c
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.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("/home/zxz/open_file_excise/exec_process/hello", "hello", NULL);
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;
}
status
:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
代码示例
exit()
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("hello\n");
printf("world");
exit(0);
// exit(0) 等价于 return 0,于是不再执行以下代码
printf("byebye");
return 0;
}
_exit()
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("hello\n");
printf("world");
_exit(0);
// exit(0) 等价于 return 0,于是不再执行以下代码
printf("byebye");
return 0;
}
注意:exit()
在调用 _exit()
之前会进行刷新I/O缓冲,由于当 std::endl
或者 \n
被输出时,缓冲区会被刷新,所以数据会被立即显示在屏幕上,而直接调用 _exit()
不会进行刷新I/O缓冲,所以当 std::endl
或者 \n
未被输出时数据不会被显示在屏幕上。
父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process) 。
代码示例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
// 创建子进程
pid_t pid = fork();
// 判断是父进程还是子进程
if (pid > 0)
{
// 如果大于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 (int i = 0; i < 5; i++)
{
printf("i : %d, pid : %d\n", i, getpid());
}
return 0;
}
孤儿进程没有什么危害,已领养孤儿进程的父进程(内核的 init 进程)会循环地 wait()
已经退出的子进程,最终会处理子进程直到其结束生命周期。
每个进程结束之后,都会释放自己地址空间中的用户区数据, 内核区的 PCB没有办法自己释放掉,需要父进程去释放。进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程(Zombie Process)。
代码示例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
// 创建子进程
pid_t pid = fork();
// 判断是父进程还是子进程
if (pid > 0)
{
while (1)
{
// 如果大于0,返回的是创建的子进程的进程号,当前是父进程
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
}
else if (pid == 0)
{
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
}
for (int i = 0; i < 5; i++)
{
printf("i : %d, pid : %d\n", i, getpid());
}
return 0;
}
父进程执行完,子进程还在执行未释放内核资源(无限循环)
新建一个终端输入 ps aux
查看僵尸进程
一般调试使用 Ctrl + C
杀掉僵尸进程(kill -9
杀不死)
ps:如果父进程不调用 wait()
或 waitpid()
的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
父进程可以通过调用 wait()
或 waitpid()
得到它的退出状态同时彻底清除掉这个进程。
pid_t wait(int *wstatus);
wstatus
:退出时的状态信息,传入的是一个int类型的地址,传出参数调用 wait()
的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号才被唤醒(相当于继续往下执行)。如果没有子进程或子进程都结束了,会立即返回 -1 。
代码示例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main()
{
// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;
for (int i = 0; i < 5; i++)
{
pid = fork();
if (pid == 0)
{
break;
}
}
if (pid > 0)
{
// 父进程
while (1)
{
printf("parent, pid = %d\n", getpid());
int ret = wait(NULL);
if (ret == -1)
{
break;
}
printf("child die, pid = %d\n", ret);
sleep(1);
}
}
else if (pid == 0)
{
// 子进程
while (1)
{
printf("child, pid = %d\n", getpid());
sleep(1);
}
}
return 0;
}
执行代码后ps aux查看创建的5个子进程都处于阻塞状态
选择第一个子进程杀掉
kill -9 84790
退出
WIFEXITED(status)
:非0,进程正常退出WEXITSTATUS (status)
:如果上面宏为真,获取进程退出的状态( exit()
的参数)终止
WIFSIGNALED(status)
:非0,进程异常终止WTERMSIG(status)
:如果上面宏为真,获取使进程终止的信号编号暂停
IFSTOPPED (status)
:非0,进程处于暂停状态WSTOPSIG(status)
:如果上面宏为真,获取使进程暂停的信号的编号WIFCONTINUED (status)
:非0,进程暂停后已经继续运行代码示例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;
for (int i = 0; i < 5; i++)
{
pid = fork();
if (pid == 0)
{
break;
}
}
if (pid > 0)
{
// 父进程
while (1)
{
printf("parent, pid = %d\n", getpid());
// int ret = wait(NULL);
int st;
int ret = wait(&st);
if (ret == -1)
{
break;
}
if (WIFEXITED(st))
{
// 是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if (WIFSIGNALED(st))
{
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}
printf("child die, pid = %d\n", ret);
sleep(1);
}
}
else if (pid == 0)
{
// 子进程
while (1)
{
printf("child, pid = %d\n", getpid());
sleep(1);
}
exit(0);
}
return 0;
}
使用信号杀掉子进程
pid_t waitpid(pid_t pid, int *wstatus, int options);
pid
:
pid > 0
:某个子进程的pidpid = 0
:回收当前进程组的任意子进程pid = -1
:回收所有的子进程,相当于wait()pid < -1
:某个进程组的组id的绝对值,回收指定进程组中的子进程wstatus
:退出时的状态信息,传入的是一个int类型的地址,传出参数options
:设置阻塞或者非阻塞
0
:阻塞WNOHANG
:非阻塞>0
:返回子进程的id=0
: options
= WNOHANG
,表示还有子进程活着= -1
:错误,或者没有子进程了代码示例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;
for (int i = 0; i < 5; i++)
{
pid = fork();
if (pid == 0)
{
break;
}
}
if (pid > 0)
{
// 父进程
while (1)
{
printf("parent, pid = %d\n", getpid());
sleep(1);
// int ret = wait(NULL);
int st;
// int ret = waitpid(-1, &st, 0);
// 非阻塞,父进程不用挂起还可以执行
int ret = waitpid(-1, &st, WNOHANG);
if (ret == -1)
{
break;
}
if (ret == 0)
{
// 说明还有子进程存在
continue;
}
else if (ret > 0)
{
if (WIFEXITED(st))
{
// 是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if (WIFSIGNALED(st))
{
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}
}
printf("child die, pid = %d\n", ret);
}
}
else if (pid == 0)
{
// 子进程
while (1)
{
printf("child, pid = %d\n", getpid());
sleep(1);
}
exit(0);
}
return 0;
}
使用信号杀死子进程
进程间通信(Inter-Process Communication,IPC)是指不同执行的进程之间进行数据交换和信息共享的机制。
进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联.不能在一个进程中直接访问另一个进程的资源。但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。
进程间通信目的
管道也叫无名(匿名)管道,它是UNIX系统IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。 匿名管道是一种单向通信管道,允许一个进程将数据写入管道的一端,另一个进程从管道的另一端读取数据。
代码示例
统计一个目录文件的数目,为了执行该命令 shell 创建了两个进程来分别执行 ls
和 wc
ls | wc -l
lseek()
来随机的访问数据或者说匿名管道只能在具有公共祖先的进程之间使用。
其实管道类似于文件,管道读写端获取数据类似于文件的读写。
底层为线性队列,逻辑类似循环队列,读写顺序相同。
int pipe(int pipefd[2]);
int pipefd[2]
:这个数组是一个传出参数
pipefd[0]
:对应的是管道的读端pipefd[1]
:对应的是管道的写端注意:匿名管道只能用于具有关系的进程之间的通信(父子进程、兄弟进程)。管道中如果没有信息时 read 操作是阻塞的,管道中如果信息已满 write 操作是阻塞,不能够同时进行读写,否则会一直阻塞。 sleep()
不能放在 read()
下面否则会造成自写自读(注释掉 sleep()
也会发生)。
代码示例
不停读写的父子进程
// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if (pid > 0)
{
// 父进程
printf("i am parent processe, pid = %d\n", getpid());
char buf[1024] = {0};
while (1)
{
// 从管道中读取数据
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());
// 向管道中写入数据
char *str = "hello,i am parent";
write(pipefd[1], str, strlen(str));
sleep(1);
}
}
else if (pid == 0)
{
// 子进程
printf("i am child process, pid = %d\n", getpid());
char buf[1024] = {0};
while (1)
{
// 向管道中写入数据
char *str = "hello, i am child";
write(pipefd[1], str, strlen(str));
sleep(1);
// 从管道中读取数据
int len = read(pipefd[0], buf, sizeof(buf));
printf("child pecv: %s, pid = %d\n", buf, getpid());
}
}
return 0;
}
查看大小
ulimit -a
使用函数获取
long fpathconf(int fd, int name);
fd
:要查询的文件描述符name
:表示要查询的属性(以_PC_为前缀的常量)以下为常用属性:
_PC_LINK_MAX
:文件的最大链接数_PC_MAX_CANON
:规范输入队列中的最大字节数_PC_MAX_INPUT
:输入队列中的最大字节数_PC_NAME_MAX
:文件名的最大长度_PC_PATH_MAX
:文件路径的最大长度_PC_PIPE_BUF
:管道缓冲区的最大大小_PC_CHOWN_RESTRICTED
:指示是否允许更改文件所有者long
类型);如果失败返回 -1 并设置errno代码示例
#include <unistd.h>
#include <stdio.h>
int main(){
int pipefd[2];
int ret = pipe(pipefd);
// 获取管道的大小
long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
printf("pipe size : %ld\n", size);
return 0;
}
修改大小
ulimit -p
问题
接上一章节的代码。如果注释掉 sleep()
会直接转变成第一种自写自读情况:
解决办法
将代码改写成第三种情况
代码示例
// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if (pid > 0)
{
// 父进程
printf("i am parent processe, pid = %d\n", getpid());
// 关闭写端
close(pipefd[1]);
char buf[1024] = {0};
while (1)
{
// 从管道中读取数据
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());
}
}
else if (pid == 0)
{
// 子进程
printf("i am child process, pid = %d\n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while (1)
{
// 向管道中写入数据
char *str = "hello, i am child\n";
write(pipefd[1], str, strlen(str));
}
}
return 0;
}
实现 ps aux | grep xxx
父子进程间通信
pipe()
、execlp()
、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[4096] = {0};
int len = -1;
// size-1 表示减去字符串结束符 "\0"
while ((len = read(fd[0], buf, sizeof(buf)-1)) > 0)
{
// 过滤数据输出
printf("%s", buf);
memset(buf, 0, 4096);
}
// 回收子进程资源
wait(NULL);
}else if(pid == 0)
{
// 子进程
// 关闭读端
close(fd[0]);
// 文件描述符的重定向 stdout_fileno -> fd[1]
dup2(fd[1], STDOUT_FILENO);
// 执行 ps aux
int flag = execlp("ps", "ps", "aux", NULL);
if (flag == -1)
{
perror("execlp");
exit(0);
}
}else
{
perror("fork");
exit(0);
}
return 0;
}
ps:暂未实现grep功能,后续更新。4096其实并不是管道内存极限,而是超过4096的数据是会分包的也是保证其原子操作。
如果想在终端输出真正所想的大小(< 4096),让 len = read(fd[0], buf, sizeof(buf)-1)
别在循环里就行。
使用管道(包括匿名和有名)时,需注意以下几种特殊情况(假设都是阻塞 I/O 操作)
总结:
代码示例
// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
/*
设置管道非阻塞
fcntl
*/
int main()
{
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if (pid > 0)
{
// 父进程
printf("i am parent processe, pid = %d \n", getpid());
// 关闭写端
close(pipefd[1]);
char buf[1024] = {0};
// 获取原来的flag
int flags = fcntl(pipefd[0], F_GETFL);
// 修改flag的值
flags |= O_NONBLOCK;
// 设置新的flag
fcntl(pipefd[0], F_SETFL, flags);
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);
}
}
else if (pid == 0)
{
// 子进程
printf("i am child process, pid = %d \n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while (1)
{
// 向管道中写入数据
char *str = "hello, i am child \n";
write(pipefd[1], str, strlen(str));
sleep(3);
}
}
return 0;
}
有名管道(FIFO)又称FIFO文件,它提供了一个路径名与之关联,只要可以访问该路径就能够通过 FIFO 相互通信。
有名管道打开方式同打开普通文件一样,一旦打开就能在它上面使用与操作匿名管道和其他文件的系统调用一样的 I/O 系统函数了。
命令创建
一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open()
打开它,常见的文件 I/0 函数都可用于 FIFO。如: close()
、read()
、write()
、unlink()
等
mkfifo filename
函数创建
int mkfifo(const char *pathname, mode t mode);
pathname
:管道名称的路径mode
:文件的权限(和 open()
的 mode
是一样的)代码示例
write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
// 向管道中写数据
int main(){
// 1. 判断文件是否存在
int flag = access("fifo_test", F_OK);
if (flag == -1)
{
printf("管道不存在!请创建管道\n");
// 1.1 不存在则创建管道文件
int ret = mkfifo("fifo_test", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(0);
}
}
// 2. 以只写方式打开管道
int fifo_fd = open("fifo_test", O_WRONLY);
if (fifo_fd == -1)
{
perror("open");
exit(0);
}
// 3. 写数据
for (int i = 0; i < 100; i++)
{
char buf[1024];
sprintf(buf,"hello,%d\n", i);
printf("write data:%s\n", buf);
write(fifo_fd, buf, strlen(buf));
sleep(1);
}
// 4. 关闭FIFO文件描述符
close(fifo_fd);
return 0;
}
read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
// 从管道中读数据
int main(){
// 1. 打开管道文件
int fifo_fd = open("fifo_test", O_RDONLY);
if(fifo_fd == -1){
perror("open");
exit(0);
}
// 2. 读取数据
while (1)
{
char buf[1024] = {0};
int len = read(fifo_fd, buf, sizeof(buf));
if (len == 0)
{
printf("写端已断开连接...\n");
break;
}
printf("recv buf:%s\n", buf);
}
// 3. 关闭文件描述符
close(fifo_fd);
return 0;
}
两个进程运行
注意:
代码示例
chatA.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(){
// 1.判断有名管道是否存在
int ret = access("fifo1", F_OK);
if (ret == -1)
{
// 文件不存在
printf("管道文件不存在,创建对应有名管道\n");
ret = mkfifo("fifo1", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(0);
}
}
// 2.以只写的方式打开管道fifo1
int w_fd = open("fifo1", O_WRONLY);
if (w_fd == -1)
{
perror("open");
exit(0);
}
printf("管道fifo1打开成功,等待写入...\n");
// 3.以只读的方式打开管道fifo2
int r_fd = open("fifo2", O_RDONLY);
if (r_fd == -1)
{
perror("open");
exit(0);
}
printf("管道fifo2打开成功,等待读取...\n");
char buf[128];
// 4.循环地写读数据
while(1)
{
memset(buf, 0, 128);
// 4.1.获取标准输入的数据
fgets(buf, 128, stdin);
// 4.2.写数据
ret = write(w_fd, buf, strlen(buf));
if (ret == -1)
{
perror("write");
exit(0);
}
// 4.3.读数据
memset(buf, 0, 128);
ret = read(r_fd, buf, 128);
if (ret == -1)
{
if (ret <= 0 )
{
perror("read");
break;
}
}
printf("chatB: %s\n",buf);
}
// 5.关闭读写文件描述符
close(r_fd);
close(w_fd);
return 0;
}
chatB.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(){
// 1.判断有名管道是否存在
int ret = access("fifo2", F_OK);
if (ret == -1)
{
// 文件不存在
printf("管道文件不存在,创建对应有名管道\n");
ret = mkfifo("fifo2", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(0);
}
}
// 2.以只读的方式打开管道fifo1
int r_fd = open("fifo1", O_RDONLY);
if (r_fd == -1)
{
perror("open");
exit(0);
}
printf("管道fifo1打开成功,等待读取...\n");
// 3.以只写的方式打开管道fifo2
int w_fd = open("fifo2", O_WRONLY);
if (w_fd == -1)
{
perror("open");
exit(0);
}
printf("管道fifo2打开成功,等待写入...\n");
char buf[128];
// 4.循环地读写数据
while(1)
{
// 4.1.读数据
memset(buf, 0, 128);
ret = read(r_fd, buf, 128);
if (ret == -1)
{
if (ret <= 0 )
{
perror("read");
break;
}
}
printf("chatA: %s\n",buf);
memset(buf, 0, 128);
// 4.1.获取标准输入的数据
fgets(buf, 128, stdin);
// 4.2.写数据
ret = write(w_fd, buf, strlen(buf));
if (ret == -1)
{
perror("write");
exit(0);
}
}
// 5.关闭读写文件描述符
close(w_fd);
close(r_fd);
return 0;
}
内存映射 (Memory-mapped I/O) 是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
void *mmap(void *addr, size t length, int prot, int flags, int fd, off_t offset);
作用:将一个文件或者设备的数据映射到内存中
参数:
addr
:NULL,由内核指定length
:要映射的数据的长度,这个值不能为0。建议使用文件的长度,获取文件的长度:stat()
、lseek()
prot
:对申请的内存映射区的操作权限,要操作映射内存,必须要有读的权限(常用:PROT_READ
、PROT_READ | PROT_WRITE
)
PROT_EXEC
:可执行权限PROT_READ
:读权限PROT_WRITE
:写权限PROT_NONE
:没有权限flags
:
MAP_SHARED
:映射区的数据会自动和磁盘文件进行同步,进程间通信必须要设置这个选项MAP_PRIVATE
:不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件(copy-on-write)fd
:需要映射的那个文件的文件描述符
prot
有冲突prot
:PROT_READ
对应 open:只读/读写prot
:PROT_READ | PROT_WRITE
对应 open:读写offset
:偏移量,一般不用。必须指定的是4096 的整数倍,0表示不偏移。返回值:如果成功返回创建的内存的首地址;如果失败返回 MAP_FAILED
((void*) -1
),并设置errno
int munmap(void *addr, size t length);
addr
:要释放内存的首地址length
:要释放的数据的长度(内存大小),这个值不能为0。建议使用文件的长度,获取文件的长度:stat()
、lseek()
实现原理
进程1
:通过磁盘文件创建内存映射区,得到一个操作这块内存的指针进程2
:通过磁盘文件创建内存映射区,得到一个操作这块内存的指针注意:内存映射区通信是非阻塞的
代码示例
有关系的进程间通信
#include <stdio.h>
#include <wait.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.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, this's son!!!");
}
else
{
perror("fork");
exit(0);
}
// 4.释放内存映射区
munmap(ptr, size);
return 0;
}
1.如果对 mmap()
的返回值 ptr
做++操作(ptr++
),munmap()
是否能够成功?
答:可以做++操作,但不能成功释放,必须传递首地址。
2.如果 open()
时 O RDONLY
,mmap()
时 prot
指定 PROT_READ | PROT_WRITE
会怎样?
答:产生错误返回 MAP_FAILED
,建议open()
中权限和 prot
权限保持一致。
3.如果文件偏移量为1000会怎样?
答:偏移量必须是4096的整数倍,返回 MAP_FAILED
4.mmap()
什么情况下会调用失败?
答:① length
= 0;②prot
只指定了写权限;③产生上述第二个问题
5.可以 open()
的时候 O_CREAT
一个新文件来创建映射区吗?
答:可以的,但创建的文件大小为0不行(可使用 lseek()
和 truncate()
进行扩展)。
6.mmap()
后关闭文件描述符,对 mmap()
映射有没有影响?
答:没有影响,映射区还是存在,只是创建映射区的 fd
关闭了而已
7.对 ptr
越界操作会怎样?
答:越界操作操作的是非法内存,会产生段错误
思路步骤
代码示例
#include <stdio.h>
#include <wait.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(){
// 1.对原始的文件进行内存映射
int fd = open("english.txt", O_RDWR);
if (fd == -1)
{
perror("open");
exit(0);
}
// 获取源文件大小
int len = lseek(fd, 0, SEEK_END);
// 2.创建一个新文件(拓展该文件)
int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0644);
if (fd1 == -1)
{
perror("open");
exit(0);
}
// 对新建文件进行拓展
truncate("cpy.txt", len);
write(fd1, " ", 1);
// 3.把新文件的数据映射到内存中
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
void *ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
if(ptr1 == MAP_FAILED)
{
perror("mmap");
exit(0);
}
//内存拷贝
memcpy(ptr1, ptr, len);
// 释放资源(谁先打开先释放)
munmap(ptr1, len);
munmap(ptr, len);
// 关闭文件描述符
close(fd1);
close(fd);
return 0;
}
注意:一般来讲这样不便拷贝太大文件,防止内存不够
不需要文件实体进程一个内存映射。
可以做有关系进程(父子进程)映射。
代码示例
#define _DEFAULT_SOURCE // MAP_ANONYMOUS
#include <stdio.h>
#include <wait.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define BUF_LEN 4096
int main()
{
// 1.创建匿名映射区
void *ptr = mmap(NULL, BUF_LEN, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 2.父子间通信
pid_t pid = fork();
if (pid > 0)
{
// 父进程
strcpy((char *)ptr, "hello, world");
wait(NULL);
}
else if (pid == 0)
{
// 子进程
sleep(1);
printf("%s\n", (char *)ptr);
}
// 3.释放内存映射区
int ret = munmap(ptr, BUF_LEN);
if (ret == -1)
{
perror("munmap");
exit(0);
}
return 0;
}
信号是 Linux 进程间通信是事件发生时对进程的通知机制,有时也称之为软件中断。
信号是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入ctrl+C通常会给进程发送一个中断信号比如输入Ctrl+C
通常会给进程发送一个中断信号。
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
系统状态变化。比如 alarm()
定时器到期将引起 SIGALRM
信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
运行 kill 命令或调用 kill()
使用信号的两个目的
让进程知道已经发生了一个特定的事情
强迫进程执行它自己代码中的信号处理程序
信号的特点
简单
不能携带大量信息
满足某个特定条件才发送
优先级比较高
查看系统定义的信号列表
kill -l
ps:前 31 个信号为常规信号,其余为实时信号
编号 | 信号名称 | 对应事件 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 用户退出 shell 时由该 shell 启动的所有进程将收到这个信号 | 终止进程 |
2 | SIGINT | 当用户按下了 Ctrl+C 组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 | 终止进程 |
3 | SIGQUIT | 用户按下 Ctrl+\ 组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 | 终止进程 |
4 | SIGIIL | 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的实时信号,它们没有固定的含义 (可以由用户自定义) | 终止进程 |
ps:标红色的信号需要掌握。SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。
查看信号的详细信息
man 7 signal
信号的5中默认处理动作
ps:
Core文件保存进程异常退出的信息
#include <stdio.h>
#include <string.h>
int main() (
// 没有指向合法内存
char * buf;
strcpy(buf,"hello");
return 0;
编译生成目标文件报段错误
gcc core.c
./a.out
生成Core文件
ulimit -a
ulimit -c 1024
./a.out
调试Core文件
gdb a.out
# 进入GDB调试界面
(gdb) core-file core
信号的几种状态
int kill(pid t pid, int sig);
作用:给任何的进程或者进程组 pid,发送任何的信号 sig
参数:
pid
:
> 0
:将信号发送给指定的进程= 0
:将信号发送给当前的进程组= -1
:将信号发送给每一个有权限接收这个信号的进程<-1
:这个pid=某个进程组的ID取反 (如:-12345)sig
: 需要发送的信号的编号或者是宏值,0表示不发送任何信号返回值:如果成功返回0;如果失败返回-1,并设置errno
int raise(int sig);
作用:给当前进程发送信号
参数: sig
: 需要发送的信号的编号或者是宏值,0表示不发送任何信号
返回值:如果成功返回0;如果失败返回-1,并设置errno
void abort(void);
代码示例
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(){
pid_t pid = fork();
if (pid == 0)
{
//子进程
for (int 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;
}
unsigned int alarm(unsigned int seconds);
seconds
:倒计时的时长,单位: 秒。如果参数为0,定时器无效(不进行倒计时,不发信号),如取消一个定时器:alam(0)
SIGALARM : 默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
例子
alarm(10); //-> 返回0
//过了1秒
alarm(5); //-> 返回9
alam(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){}第一个例子
alarm(0);
// 1s电脑能数多少数//第二个例子
alarm(1);
int i = 0;
while (1)
{
printf("%i\n", i++);
}
return 0;
}
ps:其实1s内计算机真实计数远远大于 2.txt 中的数。实际的时间 = 内核时间 + 用户时间 + 消耗的时间。进行文件IO操作的时候比较浪费时间。定时器与进程的状态无关(自然定时法),无论进程处于什么状态,alarm()
都会计时。
int setitimer(int which, const struct itimerval *new_ value, struct itimerval *old value);
alarm()
,精度微秒:us,可以实现周期性定时which
:定时器以什么时间计时
ITIMER_REAL
:真实时间,时间到达,发送 SIGALRM(常用)ITIMER_VIRTUAL
:用户时间,时间到达,发送 SIGVTALRMITIMER_PROF
:以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送new_value
:设置定时器的属性old_value
:记录上一次定时器的时间参数,一般不用,指定NULL// 定时器的结构体
struct itimerval {
// 每个阶段的时间,间隔时间
struct timeval it interval;
// 延迟多长时间执行定时器
struct timeval it_value;
};
// 时间的结构体
struct timeval {
// 秒数
time_ttv_sec;
// 微秒
suseconds t tv_usec;
};
代码示例
过3秒以后,每隔2秒钟定时一次
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
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;
}
sighandler_t signal(int signum, sighandler_t handler);
signum
:要捕捉的信号handler
:捕捉到信号要如何处理
SIG_IGN
:忽略信号SIG_DFL
:使用信号默认的行为SIG_ERR
,设置errnops: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");
}
int main(){
// 注册信号捕捉
// signal(SIGALRM,SIG_IGN);
// signa1(SIGALRM,SIG_DFL);
// void (*sighandler_t)(int); 函数指针,int 类型的参数表示信号捕捉到的值
__sighandler_t flag = signal(SIGALRM, myalarm);
if (flag == SIG_ERR)
{
perror("signal");
exit(0);
}
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间
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;
}
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为
sigset_t
。
在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改
信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
工作原理
用户通过键盘ctrl + C,产生2号信号SIGINT (信号被创建)
信号产生但是没有被处理 (未决)
这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
int sigemptyset(sigset_t *set);
作用:清空信号集中的数据,将信号集中的所有的标志位置为0
参数:set
:传出参数,需要操作的信号集
返回值:成功返回0,失败返回-1
int sigfillset(sigset t *set);int sigaddset(sigset_t *set, int signum);
作用:将信号集中所有的标志位设为1
参数:
set
:传出参数,需要操作的信号集signum
: 需要设置阻塞的那个信号返回值:成功返回0,失败返回-1
int sigaddset(sigset_t *set, int signum);
作用:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
参数:
set
:传出参数,需要操作的信号集signum
: 需要设置阻塞的那个信号返回值:成功返回0;失败返回-1
int sigdelset(sigset t *set, int signum);
作用:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
参数: set
:传出参数,需要操作的信号集
返回值:成功返回0;失败返回-1
int sigismember(const sigset_t *set, int signum);
作用:判断某个信号是否阻塞
参数:
set
:需要操作的信号集signum
: 需要判断的那个信号返回值:如果成功返回1表示 signum
被阻塞,返回0表示signum
不阻塞;如果失败返回-1
代码示例
#define _DEFAULT_SOURCE
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
// 创建一个信号集
sigset_t set;
// 清空信号集的内容
sigemptyset(&set);
// 判断 SIGINT 是否在信号集 set 里
int ret = sigismember(&set, SIGINT);
if (ret == 0)
{
printf("SIGINT 不阻塞\n");
}else if (ret == 1)
{
printf("SIGINT 阻塞\n");
}
// 添加几个信号到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 判断SIGINT是否在信号集中
ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 判断SIGQUIT是否在信号集中
int jug = sigismember(&set, SIGQUIT);
if(jug == 0){
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1){
printf("SIGQUIT 阻塞\n");
}
// 从信号集中删除一个信号
sigdelset(&set, SIGQUIT);
// 判断SIGQUIT是否在信号集中
jug = sigismember(&set, SIGQUIT);
if(jug == 0){
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1){
printf("SIGQUIT 阻塞\n");
}
return 0;
}
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
int sigpending(sigset_t *set);
作用:获取内核中的未决信号集
参数:set
:传出参数,保存的是内核中的未决信号集中的信息。
返回值:如果成功返回0;如果失败返回-1,并设置errno
代码示例
编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕设置某些信号是阻塞的,通过键盘产生这些信号
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
// 设置2、3号信号阻塞
sigset_t set;
sigemptyset(&set);
// 将2号和3号信号添加到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
int num = 0;
while (1)
{
num++;
// 获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);
// 遍历前32位
for (int i = 1; i <= 32; i++)
{
if (sigismember(&pendingset, i) == 1)
{
printf("1");
}
else if (sigismember(&pendingset, i) == 0)
{
printf("0");
}
else
{
perror("sigismember");
exit(0);
}
}
printf("\n");
sleep(1);
if (num == 10)
{
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
作用:检查或者改变信号的处理。信号捕捉
参数:
signum
:需要捕捉的信号的编号或者宏值(信号的名称)act
:捕捉到信号之后的处理动作oldact
:上一次对信号捕捉相关的设置,一般不使用,传递NULL返回值:如果成功返回0;如果失败返回-1,并设置errno
sigaction 结构体
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);
代码示例
#define _DEFAULT_SOURCE
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num)
{
printf("捕捉到了信号的编号是: %d\n", num);
printf("xxxxxxx\n");
}
int main(){
// 注册信号捕捉
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
// 清空临时阻塞信号集
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
__sighandler_t flag = signal(SIGALRM, myalarm);
if (flag == SIG_ERR)
{
perror("signal");
exit(0);
}
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间
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;
}
SIGCHLD信号产生的条件
ps:以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号。可以使用SIGCHLD信号解决僵尸进程的问题。
代码示例
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
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;
}
共享内存是一种用于多进程或多线程之间共享数据的机制,它允许不同的进程或线程在物理内存创建一个共享区域(段)。
由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
shmget()
创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。shmat()
来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。shmat()
调用返回的 addr
值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。shmdt()
来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。shmctl()
来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存才会销毁。只有—个进程需要执行这一步int shmget(key_t key, size_t size, int shmflg);
作用:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识
新创建的内存段中的数据都会被初始化为0
参数:
key
:key_t
类型是一个整形,通过这个找到或者创建一个共享内存。size
:共享内存的大小shmflg
:属性
IPC_CREAT | IPC_EXCL | 0664
返回值:如果成功>0
返回共享内存的引用的ID(后面操作共享内存都是通过这个值);如果失败返回-1,并设置errno
void *shmat(int shmid, const void *shmaddr, int shmflg);
作用:和当前的进程进行关联-参数
参数:
shmid
:- shmid :共享内存的标识(ID),由shmget
返回值获取shmaddr
:申请的共享内存的起始地址,指定NULL,内核指定shmflg
:对共享内存的操作
返回值:如果成功>0
返回共享内存的起始地址;如果失败返回(void*)-1,并设置errno
int shmdt(const void *shmaddr);
作用:解除当前进程和共享内存的关联
参数:shmaddr
:共享内存的首地址
返回值:如果成功返回0;如果失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
:共享内存的IDcmd
:要做的操作
buf
:需要设置或者获取的共享内存的属性信息
代码示例
write_shm.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main()
{
// 1.创建一个共享内存
int shmid = shmget(100, 4096, IPC_CREAT | 0664);
// 2.和当前进程进行关联
void *ptr = shmat(shmid, NULL, 0);
char *str = "helloworld";
// 3.写数据
memcpy(ptr, str, strlen(str) + 1);
printf("按任意键继续\n");
getchar();
// 4.解除关联
shmdt(ptr);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
read_shm.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
// 1.获取一个共享内存
int shmid = shmget(100, 0, IPC_CREAT);
printf("shmid: %d\n", shmid);
// 2.和当前进程进行关联
void *ptr = shmat(shmid, NULL, 0);
// 3.读数据
printf("%s\n", (char *)ptr);
printf("按任意键继续\n");
getchar();
// 4.解除关联
shmdt(ptr);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
key_t ftok(const char *pathname, int proj_id);
int
值,生成一个共享内存的keypathname
:指定一个存在的路径proj_id
: int
类型的值,但是这系统调用只会使用其中的1个字节key_t
类型的键,表示关联到指定文件和项目ID的唯一键。打印当前系统中所有的进程间通信方式的信息
ipcs -a
打印出使用共享内存进行进程间通信的信息
ipcs -m
打印出使用消息队列进行进程间通信的信息
ipcs -q
打印出使用信号进行进程间通信的信息
ipcs -s
移除用shmkey创建的共享内存段
ipcrm -M shmkey
移除用shmid标识的共享内存段
ipcrm -m shmid
移除用msqkey创建的消息队列
ipcrm -Q msgkey
移除用msqid标识的消息队列
ipcrm -q msqid
移除用semkey创建的信号
ipcrm -s semkey
移除用semid标识的信号
ipcrm -s semid
问题1:操作系统如何知道一块共享内存被多少个进程关联?
答:共享内存维护了一个结构体struct shmid_ds
这个结构体中有一个成员 shm nattach
记录了关联的进程个数
问题2:可不可以对共享内存进行多次删除 shmctl
?
答:可以的。因为 shmctl
标记删除共享内存,不是直接删除。那什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除。当共享内存的key为0的时候,表示共享内存被标记删除了。如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。
共享内存和内存映射的区别
共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
共享内存效果更高
内存
所有的进程操作的是同一块共享内存。
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
数据安全
生命周期
在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为 shell 进程的控制终端(controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而
fork()
会复制 PCB 中的信息,因此由 shell 进程启动的其它进程的控制终端也是这个终端。
默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
在控制终端输入一些特殊的控制键可以给前台进程发信号,例如
Ctrl +C
会产生 SIGINT 信号,Ctrl +\
会产生 SIGQUIT 信号。
进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID为该进程组的 ID,新进程会继承其父进程所属的进程组ID。
进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。
进程组和会话在进程之间形成了一种两级层次关系:
进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。
会话是一组相关进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。
一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。
关系示例
以下命令的作用是在根目录下搜索所有文件和目录,将标准错误输出(STDERR)重定向到 /dev/null
忽略任何错误消息,然后通过管道将搜索结果的行数(即文件和目录的数量)计数,并在后台运行这个任务,期间允许继续使用终端。
find / 2> / dev /null | wc -l &
以下命令的作用是将 longlist
中的内容按照字母顺序排序,然后统计每个唯一项出现的次数,并以 次数 唯一项
的格式输出。
sort < longlist | uniq -c
以上两组命令关系图
终端显示
ps:执行完第一组命令后输入 fg
,命令将回到前台运行,并且可以看到它的输出,也可以在需要时终止它。
// 获取调用进程的进程组ID
pid_t getpgrp (void) ;
// 获取指定进程的进程组ID
pid_t getpgid(pid_t pid) ;
// 设置指定进程的进程组ID
int setpgid(pid_t pid, pid_t pgid) ;
// 获取指定进程的会话ID
pid_t getsid(pid_t pid) ;
// 创建一个新的会话,并返回其会话ID
pid_t setsid (void) ;
守护进程(Daemon Process) ,也就是通常说的 Daemon进程(精灵进程),是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,虽然产生条件和孤儿进程类似但并不是孤儿进程。
守护进程一般采用以 d 结尾的名字。
Linux的大多数服务器就是用守护进程实现的。比如, Internet服务器 inetd,web服务器 httpd等。
守护进程特征
fork()
,之后父进程退出,子进程继续执行。setsid()
开启一个新会话。umask
以确保当守护进程创建文件和目录时拥有所需的权限。/
。/dev /null
并使用 dup2()
使所有这些描述符指向这个设备。代码示例
写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <string.h>
// 写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
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);
// print("%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/zxz/");
// 5.关闭、重定向文件描述符
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
// 6.业务逻辑
// 6.1.捕捉定时信号
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;
// 6.2.创建定时器
setitimer(ITIMER_REAL, &val, NULL);
// 6.3.不让进程结束
while (1)
{
sleep(10);
}
return 0;
}
vim进入time文件
vim time.txt
键入 :e
重新加载文件
课程笔记源于牛客C++求职项目:https://www.nowcoder.com/courses/cover/live/504的第2章 Linux多进程开发。
由于并不是专门的进程学习课程,可能此章节老师的一些说法不太清晰或完全,我是结合《CSAPP》来学习的,多练习一下上面的题有时会醍醐灌顶,另外多使用 man
来查看系统API文档。
此笔记相对课程内容有增删仅个人复习用,若有需要仅供参考。以上所有代码和图片均为课程自带以及网络资源,欢迎各位大佬提出问题和建议,更完的笔记实时更新,后续为了能更加巩固相关知识可能会更新较慢望谅解。