image-20220709151547833

image-20220709151741790

image-20220709152001679

image-20220709152137918

image-20220709152409441

image-20220709152513447

进程的控制结构为PCB

image-20220709152551801

image-20220709152730589

image-20220709152820578


2.1 进程的状态

image-20220709152922274

image-20220709153613473

image-20220709153917036

image-20220709154108593

image-20220709155121586

image-20220709154222057

image-20220709154501219

image-20220709155630565


2.3 进程创建

image-20220709155809181

/*
#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产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
*/

当前终端pid87186创建fork子进程87631( 在父进程中 返回子进程的pid),一个父进程为87630,一个子进程87631。CPU时间片交替运行。

image-20220709161530157


2.4 父子进程关系及GDB多进程调试

image-20220709162506370

image-20220709162743575

初始值一样,后续操作不一样。

image-20220709162845593

image-20220712095224502

image-20220709164650569

父子进程间遵循读时共享写时复制的原则。只有进程空间的各段的内容要发生变化时(子进程或父进程进行写操作时,都会引起复制),才会将父进程的内容复制一份给子进程。父子进程在逻辑上仍然是严格相互独立的两个进程,各自维护各自的参数,只是在物理上实现了读时共享,写时复制。

image-20220709173503178


2.5 父子进程关系

image-20220709174528241

默认调试父进程,子进程自己跑自己的。

image-20220709174556763

image-20220712170609358


2.6 函数族

image-20220709191216787

image-20220709192101771

/*  
#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;
}

image-20220712193814626

image-20220712193751377

image-20220712184916552

hello world不在一起的原因是因为产生了孤儿进程

/*  
#include <unistd.h>
int execlp(const char *file, const char *arg, ... );
- 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
- 参数:
- file:需要执行的可执行文件的文件名
a.out
ps

- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)

- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。


int execv(const char *path, char *const argv[]);
argv是需要的参数的一个字符串数组
char * argv[] = {"ps", "aux", NULL};
execv("/bin/ps", argv);

int execve(const char *filename, char *const argv[], char *const envp[]);
char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};
*/
#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) {
// 子进程
execlp("ps", "ps", "aux", 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;
}

2.7 进程退出

上是标准C库,下是linux系统函数

image-20220709200757166

image-20220709200943834

image-20220709201041144

在程序中

孤儿进程:子进程先睡一秒,父进程运行玩死了,子进程还活着,子进程成为孤儿进程。

僵尸进程:父进程一直运行不死,子进程死了,残留内核区资源,成为僵尸进程。然后只能Ctrl+C杀死,子进程被进程号为1回收。

image-20220709201148099

正常的情况下

image-20220712195746801

image-20220712195707422

想让父进程死,就让子进程睡一秒钟

image-20220712195924079

image-20220709201834519

​ 孤儿进程的父进程号为1,父进程结束后会显示终端,由进程号为1的进程来回收子进程的资源。

image-20220709202519128

image-20220712200503016

子进程死了父进程还在进行。父进程14859处于休眠状态,子进程14860是僵尸进程。,kill -9杀不死。

image-20220712200624838


2.8 wait函数

主要针对僵尸进程。学习如何在父进程中回收子进程的资源。

image-20220712201345734

wait 能够等待子进程状态改变,包含子进程结束、被信号停止、被信号暂停。调用wait会去释放子进程的资源。父进程默认wait阻塞了,

/*
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。
参数:int *wstatus
进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
返回值:
- 成功:返回被回收的子进程的id
- 失败:-1 (所有的子进程都结束,调用函数失败)

调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.

*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int main() {

// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;

// 创建5个子进程
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; // exit(0)
}

image-20220721161212220

image-20220712204152216

此时子进程已经死掉,成为僵尸进程。当被ctrl+c结束了父进程,僵尸进程就被进程号为1的进程回收了,因为父进程一死,子进程就变成孤儿进程。但并不能每次都结束父进程,应该在父进程中做一些操作。

wait(NULL) 表示不需要子进程退出的状态

image-20220712204332100

image-20220712210245314

子进程一直在打印,父进程没有动,说明父进程阻塞了。通过kill -9 13478 ,父进程不阻塞了,就打印。

image-20220712210434033

然后全kill掉,此时没有子进程了。所有子进程都结束,则返回-1。

image-20220712204628113

image-20220712204642539

image-20220712204712745

image-20220712211021844

通过信号kill -9杀死

image-20220712205059554


2.9 waitpid函数

/*
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
- pid:
pid > 0 : 某个子进程的pid
pid = 0 : 回收当前进程组的所有子进程
pid = -1 : 回收所有的子进程,相当于 wait() (最常用)
pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
- options:设置阻塞或者非阻塞
0 : 阻塞
WNOHANG : 非阻塞
- 返回值:
> 0 : 返回子进程的id
= 0 : options=WNOHANG, 表示还有子进程或者
= -1 :错误,或者没有子进程了
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {

// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;

// 创建5个子进程
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 st;
// int ret = waitpid(-1, &st, 0);
int ret = waitpid(-1, &st, WNOHANG);

if(ret == -1) {
break;
} else 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;
}

int ret = waitpid(-1, &st, 0);阻塞情况

image-20220712215332656

image-20220712215357568

父进程非阻塞。

image-20220712215625353

image-20220712214240848

所有子进程都结束之后,再回过来 ret返回了-1值。


2.10 进程间通信 IPC

image-20220712221317444

image-20220712222017940


2.11 匿名管道(PIPE)

所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。

这两个描述符都是在一个进程里面,并没有起到进程间通信的作用,怎么样才能使得管道是跨过两个进程的呢?

我们可以使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「 fd[0]fd[1]」,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。

img

image-20220712222710566

竖线为管道符,前后各为一个指令,ls为获取当前文件列表,wc -l统计个数。写入端就是向管道注入,管道读取端就是从管道输出。

image-20220713093759069

image-20220713094021279

image-20220713094503815

父进程通过文件描述符5向管道写数据,子进程通过6从管道读数据,通过其管道5写数据,父进程通过6从管道读数据,父子进程共享文件描述符。

image-20220713094835797

管道数据结构为环形队列。

image-20220713095024204

2.12 父子进程通过匿名管道通信

数据被写到管道的写端,(应该是入端)

image-20220713095309303

子进程发送数据给父进程,父进程读取到数据 后 输出

在fork之前创建管道

image-20220713095708508

image-20220713095916244

image-20220713095929370

如果子进程写之前休眠10秒,父进程仍然是阻塞等待。read是阻塞,管道默认是阻塞,当管道没有数据时,read阻塞,管道满了,write阻塞。

image-20220713100236399

image-20220713100301798

父进程先读后写,子进程先写后读。

image-20220713100626622

image-20220713100739557

image-20220713100758213

image-20220722105708703

管道大小为4k

image-20220713101030871

image-20220713101045461

2.13 匿名管道通信案例

注释掉sleep

image-20220713101531493

输出有错误,乱码 且 收到自己发的数据,不对。

image-20220713102307570

image-20220713102327586

现在情况变成了

image-20220713102353491

所以在读的时候 父进程要关闭写端,子进程要关闭读端

image-20220713102934517

image-20220713102959385

案例



/*
实现 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 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); // 管道就4k
perror("execlp");
exit(0);
} else {
perror("fork");
exit(0);
}


return 0;
}



2.14 管道读写特点和管道设置非阻塞

管道的读写特点:
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。

2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。

3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。

4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。

总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待

写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数

父进程read阻塞

image-20220713110555043

image-20220713110514941

image-20220713110755428

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
/*
设置管道非阻塞
int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(fd[0], F_SETFL, flags); // 设置新的flag
*/
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 process, pid : %d\n", getpid());

// 关闭写端
close(pipefd[1]);

// 从管道的读取端读取数据
char buf[1024] = {0};

int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(pipefd[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);
}

} 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";
write(pipefd[1], str, strlen(str));
sleep(5);
}

}
return 0;
}

image-20220713111545212

没有数据就-1, 没有阻塞。


2.15有名管道(FIFO)

image-20220713112030962

image-20220713112131822

image-20220713112720107

image-20220713112734113

image-20220713112936238

/*
创建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("test", F_OK);
if(ret == -1) {
printf("管道不存在,创建管道\n");
ret = mkfifo("fifo1", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
return 0;
}

写数据

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

// 向管道中写数据
/*
有名管道的注意事项:
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道

读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据:
管道写端被全部关闭,read返回0,(相当于读到文件末尾)
写端没有全部被关闭,read阻塞等待

写管道:
管道读端被全部关闭,进程异常终止(收到一个SIGPIPE信号)
管道读端没有全部关闭:
管道已经满了,write会阻塞
管道没有满,write将数据写入,并返回实际写入的字节数。
*/
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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

// 从管道中读取数据
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;
}

写端停了,读端显示“写端断开连接”,程序结束。

运行读端,读端在open管道文件时阻塞了。运行写端,读端就运行了。

把读端停了,写端程序立马结束。


2.16 有名管道实现简单聊天功能

image-20220713115229732

chatA

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.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);
}
}

ret = access("fifo2", F_OK);
if(ret == -1) {
// 文件不存在
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo2", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}

// 2.以只写的方式打开管道fifo1
int fdw = open("fifo1", O_WRONLY);
if(fdw == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo1成功,等待写入...\n");
// 3.以只读的方式打开管道fifo2
int fdr = open("fifo2", O_RDONLY);
if(fdr == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo2成功,等待读取...\n");

char buf[128];

// 4.循环的写读数据
while(1) {
memset(buf, 0, 128);
// 获取标准输入的数据
fgets(buf, 128, stdin);
// 写数据
ret = write(fdw, buf, strlen(buf));
if(ret == -1) {
perror("write");
exit(0);
}

// 5.读管道数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if(ret <= 0) {
perror("read");
break;
}
printf("buf: %s\n", buf);
}

// 6.关闭文件描述符
close(fdr);
close(fdw);

return 0;
}

此时只能收一句 发一句,不能把读和写都放入同一个进程中,必定有一个阻塞。所以要把读写分别放到不同进程里。A父进程写,子进程读。B父进程读,子进程写。


2.17内存映射

类似于动态库、共享库的位置。

可以指定映射 从文件的偏移量开始的len大小的文件。

内存映射之后,当修改了内存数据,也会同步到文件当中。

image-20220713141211276

左右为进程中的内存数据。

image-20220713143305010

image-20220713141859759

映射文件到内存当中。

mmap()在调用进程的虚拟地址空间里,创建了一个映射

image-20220713142227869

  • flags:
    • map_shared 映射区的数据会自动和磁盘文件进行同步,进程间通信必须要设置这个选项。
    • map_private 不同步,内存映射区的数据修改了,对原来的文件不会修改,会重新出一个新的文件(copy on write)

image-20220713142715528

image-20220713142742847

案例需求

image-20220713142855197

image-20220713150351372

image-20220713150656773

父进程读数据,子进程写数据。以文件为桥梁。

image-20220713150921776

image-20220713150940667

1、如果对mmap的返回值ptr做++操作,可以,但是munmap错误。

2、如果open时O_RDONLY,mmap时prot参数指定PROT_READ | PROT_WRITE 会产生错误,返回MAP_FAILED;两者权限应该一致,mmap权限小于等于open才对。

3、如果文件偏移量为1000,offset必须是4k的整数倍,所以会报错返回map_failed。

4、mmap什么情况下回调用失败?第二个参数length = 0, 第三个参数prot只指定了写权限。或者和open不一致会报错。

image-20220713151649555

image-20220713151752961

使用内存映射实现文件拷贝的功能

把english.txt复制为cpy.txt

image-20220713151957260

image-20220713152315628

image-20220713152434989

image-20220713152512439

之前为文件映射,下面为匿名映射 MAP_ANONYMOUS

image-20220713152941820

内存映射为非阻塞的。 子进程读取,父进程写。

image-20220713153100366


2.19 信号概述

image-20220713153253383

image-20220713153613482

image-20220713153821911

image-20220713153901814

image-20220713153946392

image-20220713154013335

image-20220713154048465

image-20220713154055618

针对core文件

image-20220713154842933

用了buf野内存。

image-20220713154912388

要设置core文件

image-20220713154625742

image-20220713155039196

gdb a.out来调试,产生信号11。

image-20220713155148421

image-20220713154413309

image-20220713160018537

image-20220713160111686

image-20220713160135830

image-20220713160419641

image-20220713160442910


2.21 alarm函数

image-20220713160937737

alarm(100)是不阻塞的。

image-20220713161131386

image-20220713161145062

image-20220713162237088

image-20220713162352329

image-20220713162644561

image-20220713164815325

image-20220713171214771

过3s吼,每隔2秒定时一次。

image-20220713165638806

image-20220713170125850

立马调用“定时器开始了, 因为setitimer是非阻塞的。应该是每隔两秒钟发送一个信号,把进程杀死了,因为没有信号捕捉,所以没有定时的效果。


image-20220713172022840

image-20220713172712393

image-20220713182100095

image-20220713183123518


2.24信号集

image-20220713184355053

image-20220713184959698

image-20220713185257423

image-20220713185450518

image-20220713185441191

image-20220713185727380

image-20220713185630069

image-20220713185706086

image-20220713190200384

image-20220713190225161

image-20220713190258103

image-20220713190611343


之前都是对用户自定义的信号集进行操作。如果想对内核的信号集进行操作。只能sigprocmask,对系统中的阻塞信号集进行操作,想获取阻塞信号集或者设置阻塞信号集。通过sigprocmask把自定义的系统信号集设置到内核中,

image-20220714005733605

image-20220714005809132

image-20220714005919493

image-20220714012506081

image-20220714010119582

按了ctrl+c和ctrl+\

image-20220714010413244

然后只能新建会话kill -9杀死。

image-20220714010508133

或者可以输入& 以后台方式运行,还可响应其他指令。 此时再ctrl+c,或\

输入fg转为前台。

image-20220714010845622

image-20220714010911746


2.26 信号捕捉函数sigaction

signum 可以是任何有效信号,除了SIGKILL和SIGSTOP(不能被捕捉)。

image-20220714101044645

image-20220714101256982

image-20220714102818217

image-20220714103146248

image-20220714103308334


2.27 SIGCHLD信号

image-20220714104140287

使用SIGCHLD信号解决僵尸进程问题。

image-20220714105452323

image-20220714105421067

image-20220714105145067

image-20220714105512496

同时有3个子进程死亡,在未决信号集处理信号的回调函数时,无法响应其他信号。

image-20220714105747442

image-20220714105956236

会出现段错误。

image-20220714110408486

image-20220714110451684


2.28 共享内存

比内存映射效率高,内存映射需要关联一个文件,

image-20220714112130286

image-20220714113340274

image-20220714113656491

共享内存相关的函数
#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'

shmctl :创建共享内存的进程被销毁了,对共享内存是没有任何影响的。

在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);
printf("shmid : %d\n", shmid);

// 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>
#include <string.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;
}

image-20220714140417147

image-20220714140428610

之前的实验是通过随便取的100内存,应该用ftok:根据指定的路径名,和int值,生成一个共享内存的key

image-20220714140904302

问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
- shm_nattach 记录了关联的进程个数

image-20220714141051334

键100转为0x64,如果有一个读一个写,那么状态数就为2了

image-20220714141129189

如果将其中一个读进程结束了,键变成0,共享内存被标记删除。

image-20220714141403061

image-20220714141742551

ipcrm -m 4只是标记了删除,连接数没有删,当其他进程ctrl + c,状态数才变0.

问题2:可不可以对共享内存进行多次删除 shmctl
- 可以的
- 因为shmctl 标记删除共享内存,不是直接删除
- 什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除
- 当共享内存的key为0的时候,表示共享内存被标记删除了
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。
共享内存和内存映射的区别
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高
3.内存
所有的进程操作的是同一块共享内存。
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全
- 进程突然退出
共享内存还存在
内存映射区消失
- 运行进程的电脑死机,宕机了
数据存在在共享内存中,没有了
内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
5.生命周期
- 内存映射区:进程退出,内存映射区销毁
- 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。

2.30 守护进程

image-20220714145944446

image-20220714150116296

image-20220714150131600

image-20220714150140913

image-20220714150309918

image-20220714150600775

image-20220714150850261

1、在终端上 输入指令 find查找2,重定向到/dev/null 设备上,管道符会创建子进程,&在后台运行。是后台进程。

2、在shell中,默认没有会话 没有组,pid为400,创建ppid,然后创建出一个组 ,创建出的会话pid也是400,默认是后台程序。

3、运行find,创建新的进程组,首进程是658,父进程是bash 的400,组PGID是658,会话进程是400。

4、sort是前台运行,是前台运行组,而bash和find是后台进程组。

image-20220714151125316

image-20220714151211201

后台服务进程,生命周期长,不拥有控制终端。

image-20220714152110355

1、为什么 父进程退出

命令行启动进程,如果不退出父进程,父进程死了之后,shell会提供shell提示符。

使用fork可以确保子进程不会成为进程组的首进程。

2、子进程开启新会话,组id和进程id一样,又成为会话的id。要脱离控制终端。

写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。

image-20220714153957356

image-20220714154206911

image-20220714155024113

image-20220714154723472