进程的控制结构为PCB
2.1 进程的状态
2.3 进程创建
#include <sys/types.h> #include <unistd.h> #include <stdio.h>
int main() {
int num = 10;
pid_t pid = fork();
if(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(int i = 0; i < 3; i++) { printf("i : %d , pid : %d\n", i , getpid()); sleep(1); }
return 0; }
|
当前终端pid87186创建fork子进程87631( 在父进程中 返回子进程的pid),一个父进程为87630,一个子进程87631。CPU时间片交替运行。
2.4 父子进程关系及GDB多进程调试
初始值一样,后续操作不一样。
父子进程间遵循读时共享写时复制的原则。只有进程空间的各段的内容要发生变化时(子进程或父进程进行写操作时,都会引起复制),才会将父进程的内容复制一份给子进程。父子进程在逻辑上仍然是严格相互独立的两个进程,各自维护各自的参数,只是在物理上实现了读时共享,写时复制。
2.5 父子进程关系
默认调试父进程,子进程自己跑自己的。
2.6 函数族
#include <unistd.h> #include <stdio.h>
int main() {
pid_t pid = fork();
if(pid > 0) { printf("i am parent process, pid : %d\n",getpid()); sleep(1); }else if(pid == 0) {
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; }
|
hello world不在一起的原因是因为产生了孤儿进程
#include <unistd.h> #include <stdio.h>
int main() {
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系统函数
在程序中
孤儿进程:子进程先睡一秒,父进程运行玩死了,子进程还活着,子进程成为孤儿进程。
僵尸进程:父进程一直运行不死,子进程死了,残留内核区资源,成为僵尸进程。然后只能Ctrl+C杀死,子进程被进程号为1回收。
正常的情况下
想让父进程死,就让子进程睡一秒钟
孤儿进程的父进程号为1,父进程结束后会显示终端,由进程号为1的进程来回收子进程的资源。
子进程死了父进程还在进行。父进程14859处于休眠状态,子进程14860是僵尸进程。,kill -9杀不死。
2.8 wait函数
主要针对僵尸进程。学习如何在父进程中回收子进程的资源。
wait 能够等待子进程状态改变,包含子进程结束、被信号停止、被信号暂停。调用wait会去释放子进程的资源。父进程默认wait阻塞了,
#include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h>
int main() {
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 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; }
|
此时子进程已经死掉,成为僵尸进程。当被ctrl+c结束了父进程,僵尸进程就被进程号为1的进程回收了,因为父进程一死,子进程就变成孤儿进程。但并不能每次都结束父进程,应该在父进程中做一些操作。
wait(NULL) 表示不需要子进程退出的状态
子进程一直在打印,父进程没有动,说明父进程阻塞了。通过kill -9 13478 ,父进程不阻塞了,就打印。
然后全kill掉,此时没有子进程了。所有子进程都结束,则返回-1。
通过信号kill -9杀死
2.9 waitpid函数
#include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h>
int main() {
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 st; 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);阻塞情况
父进程非阻塞。
所有子进程都结束之后,再回过来 ret返回了-1值。
2.10 进程间通信 IPC
2.11 匿名管道(PIPE)
所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。
这两个描述符都是在一个进程里面,并没有起到进程间通信的作用,怎么样才能使得管道是跨过两个进程的呢?
我们可以使用 fork
创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「 fd[0]
与 fd[1]
」,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。
竖线为管道符,前后各为一个指令,ls为获取当前文件列表,wc -l统计个数。写入端就是向管道注入,管道读取端就是从管道输出。
父进程通过文件描述符5向管道写数据,子进程通过6从管道读数据,通过其管道5写数据,父进程通过6从管道读数据,父子进程共享文件描述符。
管道数据结构为环形队列。
2.12 父子进程通过匿名管道通信
数据被写到管道的写端,(应该是入端)
子进程发送数据给父进程,父进程读取到数据 后 输出
在fork之前创建管道
如果子进程写之前休眠10秒,父进程仍然是阻塞等待。read是阻塞,管道默认是阻塞,当管道没有数据时,read阻塞,管道满了,write阻塞。
父进程先读后写,子进程先写后读。
管道大小为4k
2.13 匿名管道通信案例
注释掉sleep
输出有错误,乱码 且 收到自己发的数据,不对。
现在情况变成了
所以在读的时候 父进程要关闭写端,子进程要关闭读端
案例
#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]);
dup2(fd[1], STDOUT_FILENO); execlp("ps", "ps", "aux", NULL); 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阻塞
#include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h>
int main() {
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); flags |= O_NONBLOCK; 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"; write(pipefd[1], str, strlen(str)); sleep(5); } } return 0; }
|
没有数据就-1, 没有阻塞。
2.15有名管道(FIFO)
#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>
int main() { int ret = access("test", F_OK); if(ret == -1) { printf("管道不存在,创建管道\n"); ret = mkfifo("test", 0664);
if(ret == -1) { perror("mkfifo"); exit(0); } } 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() { 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 有名管道实现简单聊天功能
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() {
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); } }
int fdw = open("fifo1", O_WRONLY); if(fdw == -1) { perror("open"); exit(0); } printf("打开管道fifo1成功,等待写入...\n"); int fdr = open("fifo2", O_RDONLY); if(fdr == -1) { perror("open"); exit(0); } printf("打开管道fifo2成功,等待读取...\n");
char buf[128];
while(1) { memset(buf, 0, 128); fgets(buf, 128, stdin); ret = write(fdw, buf, strlen(buf)); if(ret == -1) { perror("write"); exit(0); }
memset(buf, 0, 128); ret = read(fdr, buf, 128); if(ret <= 0) { perror("read"); break; } printf("buf: %s\n", buf); }
close(fdr); close(fdw);
return 0; }
|
此时只能收一句 发一句,不能把读和写都放入同一个进程中,必定有一个阻塞。所以要把读写分别放到不同进程里。A父进程写,子进程读。B父进程读,子进程写。
2.17内存映射
类似于动态库、共享库的位置。
可以指定映射 从文件的偏移量开始的len大小的文件。
内存映射之后,当修改了内存数据,也会同步到文件当中。
左右为进程中的内存数据。
映射文件到内存当中。
mmap()在调用进程的虚拟地址空间里,创建了一个映射
- flags:
- map_shared 映射区的数据会自动和磁盘文件进行同步,进程间通信必须要设置这个选项。
- map_private 不同步,内存映射区的数据修改了,对原来的文件不会修改,会重新出一个新的文件(copy on write)
案例需求
父进程读数据,子进程写数据。以文件为桥梁。
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不一致会报错。
使用内存映射实现文件拷贝的功能
把english.txt复制为cpy.txt
之前为文件映射,下面为匿名映射 MAP_ANONYMOUS 。
内存映射为非阻塞的。 子进程读取,父进程写。
2.19 信号概述
针对core文件
用了buf野内存。
要设置core文件
gdb a.out来调试,产生信号11。
2.21 alarm函数
alarm(100)是不阻塞的。
过3s吼,每隔2秒定时一次。
立马调用“定时器开始了, 因为setitimer是非阻塞的。应该是每隔两秒钟发送一个信号,把进程杀死了,因为没有信号捕捉,所以没有定时的效果。
2.24信号集
之前都是对用户自定义的信号集进行操作。如果想对内核的信号集进行操作。只能sigprocmask,对系统中的阻塞信号集进行操作,想获取阻塞信号集或者设置阻塞信号集。通过sigprocmask把自定义的系统信号集设置到内核中,
按了ctrl+c和ctrl+\
然后只能新建会话kill -9杀死。
或者可以输入& 以后台方式运行,还可响应其他指令。 此时再ctrl+c,或\
输入fg转为前台。
2.26 信号捕捉函数sigaction
signum 可以是任何有效信号,除了SIGKILL和SIGSTOP(不能被捕捉)。
2.27 SIGCHLD信号
使用SIGCHLD信号解决僵尸进程问题。
同时有3个子进程死亡,在未决信号集处理信号的回调函数时,无法响应其他信号。
会出现段错误。
2.28 共享内存
比内存映射效率高,内存映射需要关联一个文件,
共享内存相关的函数 #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() {
int shmid = shmget(100, 4096, IPC_CREAT|0664); printf("shmid : %d\n", shmid); void * ptr = shmat(shmid, NULL, 0);
char * str = "helloworld";
memcpy(ptr, str, strlen(str) + 1);
printf("按任意键继续\n"); getchar();
shmdt(ptr);
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() {
int shmid = shmget(100, 0, IPC_CREAT); printf("shmid : %d\n", shmid);
void * ptr = shmat(shmid, NULL, 0);
printf("%s\n", (char *)ptr); printf("按任意键继续\n"); getchar();
shmdt(ptr);
shmctl(shmid, IPC_RMID, NULL);
return 0; }
|
之前的实验是通过随便取的100内存,应该用ftok:根据指定的路径名,和int值,生成一个共享内存的key
问题1:操作系统如何知道一块共享内存被多少个进程关联? - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch - shm_nattach 记录了关联的进程个数
|
键100转为0x64,如果有一个读一个写,那么状态数就为2了
如果将其中一个读进程结束了,键变成0,共享内存被标记删除。
ipcrm -m 4只是标记了删除,连接数没有删,当其他进程ctrl + c,状态数才变0.
问题2:可不可以对共享内存进行多次删除 shmctl - 可以的 - 因为shmctl 标记删除共享内存,不是直接删除 - 什么时候真正删除呢? 当和共享内存关联的进程数为0的时候,就真正被删除 - 当共享内存的key为0的时候,表示共享内存被标记删除了 如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。
|
共享内存和内存映射的区别 1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外) 2.共享内存效果更高 3.内存 所有的进程操作的是同一块共享内存。 内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。 4.数据安全 - 进程突然退出 共享内存还存在 内存映射区消失 - 运行进程的电脑死机,宕机了 数据存在在共享内存中,没有了 内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。 5.生命周期 - 内存映射区:进程退出,内存映射区销毁 - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机 如果一个进程退出,会自动和共享内存进行取消关联。
|
2.30 守护进程
1、在终端上 输入指令 find查找2,重定向到/dev/null 设备上,管道符会创建子进程,&在后台运行。是后台进程。
2、在shell中,默认没有会话 没有组,pid为400,创建ppid,然后创建出一个组 ,创建出的会话pid也是400,默认是后台程序。
3、运行find,创建新的进程组,首进程是658,父进程是bash 的400,组PGID是658,会话进程是400。
4、sort是前台运行,是前台运行组,而bash和find是后台进程组。
后台服务进程,生命周期长,不拥有控制终端。
1、为什么 父进程退出
命令行启动进程,如果不退出父进程,父进程死了之后,shell会提供shell提示符。
使用fork可以确保子进程不会成为进程组的首进程。
2、子进程开启新会话,组id和进程id一样,又成为会话的id。要脱离控制终端。
写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。