linux 应用开发笔记---【进程】

2023-12-18 21:48:02

1.进程的概念:

何为进程:

进程是一个应用程序的实例。也就是系统中真正运行的应用程序,程序一旦运行就是进程,进程是一个动态过程,它是程序的一次运行结果,而非静态文件

进程的生命周期:? ?从程序启动到程序退出的这段时间

进程号(process ID):

Linux系统下的每一个进程都要一个进程号(PID),进程号是一个正数,用于唯一标识系统中的某一个进程

pid_t getpid(void);


获取该进程的pid号
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
        pid_t pid = getpid();
        printf("本进程的 PID 为: %d\n", pid);
        exit(0);
}

运行结果:

pid_t getppid(void);


获取父进程的进程号

运行main()函数之前会有一段引导代码函数,这段代码并不需要我们自己编写,而是在编译,链接我们的应用程序的时候由链接器将这段引导代码链接到我们的链接程序中,构成最终的可执行文件,也就是最后的程序

exit()和_exit()的区别;

1.exit()是库函数,_exit()它是一个系统调用

2.exit()是会调用_exit()以及刷新文件缓存区,确保缓存区的内容保存回到文件,_exit()会直接关闭进程,可能会导致文件的丢失

2.进程的环境变量

每个进程都有一个与其相关的环境变量,环境变量以字符串的形式存储在一个字符串组列表【名称 = 值】

可以使用 export 添加环境变量? ? ?export -n 删除环境变量

char *getenv(const char *name);


获取进程的环境变量

3.添加/删除/修改环境变量

putenv()函数可以向进程的环境变量数组添加一个新的环境变量,或者修改一个已经存在的环境变量的数值

int putenv(char *string);
int setenv(const char *name, const char *value, int overwrite);

name:需要添加或修改的环境变量名称

value:环境变量的值

overwrite:当为0的时候,也就不会改变当前的环境变量的数值     
当不为0的时候,若name的环境变量不变,则添加。若存在,则覆盖

移除name标识的环境变量

int unsetenv(const char *name);
清空环境变量:
int clearenv(void);

注意:调用setenv的时候,会为环境变量分配一块内存缓冲区,但是clearenv却不会释放这块内存缓存区,也就是会不断的产生内存泄漏

4.fork()创建子进程

linux所有的进程都是由父进程创建来的

init是所有进程的祖先进程

调用fork函数的进程被称为父进程,调用fork系统调用创建一个新的进程 ,被成为子进程,为父子进程关系,调用fork后会产生两个返回值,子进程和父进程分别返回一次,并且分别返回一个0及大于0的整数,但是0是子进程的返回值,大于0是父进程返回的

父子进程共享代码段,但是不共享数据段,堆,栈等,而子进程拥有父进程的数据段,堆,栈等副本

子进程从fork调用返回后的代码开始运行

子进程:被创建出来后,这便是一个独立的进程,拥有自己独立的进程空间,系统内唯一的进程号,拥有自己独立的PCB,子进程会被内核同等调度执行,参与到系统进程调度中。

fork后子进程会继承父进程绑定的信号处理函数,若调用exec加载新程序后,就不会继承这个信号处理函数了

fork后子进程会继承父进程的信号掩码,执行exec后仍继承这个信号掩码

进程空间:

在linux系统中,进程与进程之间,进程与内核之间都是相互隔离的,各自在各自的进程空间中运行,新进程被创建出来之后,便是一个独立的进程,拥有自己独立的进程空间,拥有唯一的进程号(PID),拥有自己独立的PCB【进程控制块】,新进程会被内核同等调度执行,参与到系统调用中

文件共享:

父子进程对应的文件描述符指向了相同的文件表,所以子进程改变文件的位置,子进程继承了父进程打开所有的文件描述符(父进程文件描述符的副本)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
        pid_t pid;
        int fd;
        int i;
        fd = open("./test.txt", O_RDWR | O_TRUNC);
        if (0 > fd) {
                perror("open error");
                exit(-1);
        }
        pid = fork();
        switch (pid) {
                case -1:
                        perror("fork error");
                        close(fd);
                        exit(-1);
                case 0:
                        /* 子进程 */
                        for (i = 0; i < 4; i++) //循环写入 4 次
                        write(fd, "1122", 4);
                        close(fd);
                        _exit(0);
        default:
        /* 父进程 */
                for (i = 0; i < 4; i++) //循环写入 4 次
                write(fd, "AABB", 4);
                close(fd);
                exit(0);
        }
}

运行结果:

?vfork()函数

pid_t vfork(void);
  • vfork创建新进程的主要目的在于调用exec函数执行另外的一个新程序,在没调用exec或exit之前,子进程的运行是与父进程共享数据段的;
  • vfork调用中,子进程先运行,父进程挂起,直到子进程调用exec或者exit,在这以后,父子进程的执行顺序不再被限制。
  • vfork本身就是为exec()而生,因为之前的fork()去拷贝父进程的进程环境,在调用exec()后显得毫无作用,且低效,所以共享才更合适。

5.父子进程的竞争关系?

当父进程中使用fork()创建了子进程,两者都会被系统调度正常运行,但是谁率先访问,确是不确定的

6.进程的终止

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
        printf("Hello World!");
        switch (fork()) {
                case -1:
                        perror("fork error");
                        exit(-1);
                case 0:
                /* 子进程 */
                        exit(0);
                default:
                /* 父进程 */
                        exit(0);
        }
}

上述代码printf将数据放入到缓冲区,然后创建子进程会复制缓冲区的内容,所以子父进程调用exit(0)的时候,就都会刷新缓冲区显示字符串,所以就会显示两次

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
        printf("Hello World!\n");
        switch (fork()) {
                case -1:
                        perror("fork error");
                        exit(-1);
                case 0:
                /* 子进程 */
                        exit(0);
                default:
                /* 父进程 */
                        exit(0);
        }
}

给printf后面加入了\n,因为标准输出设备采用的是行缓冲的方式?,当读取到\n的时候,会读取缓冲区的内容,因为缓冲区没有数据了,所以子进程无法读取数据,所以只打印一遍

在子进程中最好不要去使用exit(0),因为会刷新缓冲区,导致父进程出现问题,使用_exit(0)才合适

?7.监视子进程

1.wait()函数

pid_t wait(int *status);



当status不为空:


? WIFEXITED(status):如果子进程正常终止,则返回 true;
? WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit()
时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过
WEXITSTATUS 宏转换;
? WIFSIGNALED(status):如果子进程被信号终止,则返回 true;
? WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过
此宏获取终止子进程的信号;
? WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;

1.等待任意子进程的结束,然后回收子进程的终止状态信息,但是调用wait()一次只可以运行一次,也就是只能回收一个子进程

2.当所有的子进程都在运行中,则wait()会一直阻塞等待,直到某一个子进程的结束

2.waitpid()函数

pid_t waitpid(pid_t pid, int *status, int options);


? 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
? 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
? 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;
? 如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。



? WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等
待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没
有发生改变。
? WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进
程状态信息;
? WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。
从以上的介绍可知,waitpid()在功能上要强于 wait()函数,它弥补了 wait()函数所带来的一些限制,具体
在实际的编程使用当中,可根据自己的需求进行选择。

3.waitid()函数

man 2 waitid    自行查看

8.僵尸进程与孤儿进程

1.孤儿进程

父进程先于子进程结束,此时子进程就变成一个“孤儿”,所有的孤儿进程多自动成为init()【进程号为1】的子进程,当成“养父”

2.僵尸进程

进程结束之后,通常需要其父进程收尸,也就是,子进程先于父进程结束,此时父进程还没有来得及去调用 wait()...等收尸函数,子进程就变成了“僵尸状态”,紧接着,要么是父进程去调用wait()函数去收尸,当父进程没有去调用,且父进程结束了,则“养父”init进程会自动调用wait(),结束僵尸进程

僵尸进程无法通过信号杀死【SIGKILL也不行】,如果父进程没有及时的清理掉僵尸进程,则会阻碍新的进程的创建,毕竟内存有限

SIGCHLD信号

1.当父进程在某个子进程终止时,父进程会受到SIGCHLD信号

2.当父进程的某个子进程因收到信号而停止或恢复时,内核也可能向父进程发送信号,因为子进程的终止是异步事件,因为父进程是之前无法得知,但是父进程却不能一直调用wait()去阻塞等待,但是不可能一直等待。

所以,当子进程的状态改变的时候,父进程会收到SIGCHLD信号,去捕获,绑定信号函数

当调用信号处理函数的时候,会暂时将当前真正处理的信号添加到信号掩码中,当SIGCHLD正在为一个已经终止的子进程收尸的时候,如果此时有两个子进程结束了,也就是产生了两个SIGCHLD信号,但是一次的SIGCHLD的信号会被丢失,也就是说,父进程最终也只能接收一次SIGCHLD信号,那么会一个被漏掉

解决方法:在 SIGCHLD 信号处理函数中循环以非阻塞方式来调用 waitpid(),直至再无其它终止的子进程需要处理为止

while (waitpid(-1, NULL, WNOHANG) > 0)
continue;

9.执行新程序【联系vfork】

execve()函数

int execve(const char *filename, char *const argv[], char *const envp[]);



filename:参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是
相对路径。
argv:参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main(int argc, 
char *argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。
argv[0]对应的便是新程序自身路径名。
envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程
序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value

10.进程状态与关系

? 就绪态( Ready ):指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU 就能够直接运行;意味着该进程已经准备好被 CPU 执行,当一个进程的时间片到达,操作系统调度程序会从就绪态链表中调度一个进程;
? 运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态
? 僵尸态:僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”
? 可中断睡眠状态:可中断睡眠也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒
? 不可中断睡眠状态:不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件 成立才能结束睡眠状态。把浅度睡眠和深度睡眠统称为等待态(或者叫阻塞态),表示进程处于一 种等待状态,等待某种条件成立之后便会进入到就绪态;所以,处于等待态的进程是无法参与进程系统调度的
? 暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 SIGSTOP信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号
进程关系:
1.无关系
2.父子进程关系
3.进程组
? 每个进程必定属于某一个进程组、且只能属于一个进程组
? 每一个进程组有一个组长进程,组长进程的 ID 就等于进程组 ID
? 在组长进程的 ID 前面加上一个负号即是操作进程组
? 组长进程不能再创建新的进程组
? 只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关
? 一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离开该进程组
? 默认情况下,新创建的进程会继承父进程的进程组 ID

pid_t getpgid(pid_t pid);
pid_t getpgrp(void);




pid为0,则表示获取调用者进程的进程组
加入一个现有的进程组或创建一个新的进程组





int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void);



11.守护进程

守护进程也被称作精灵进程

长期运行。 守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终 止,否则直到系统关机都会保持运行。与守护进程相比,普通进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但守护进程不受用户登录注销的影响,它们将会一直运行着,直到系统关机。
? 与控制终端脱离。 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都 会依附于这个终端,这是上一小节给大家介绍的控制终端,也就是会话的控制终端。当控制终端被 关闭的时候,该会话就会退出,由控制终端运行的所有进程都会被终止,这使得普通进程都是和运 行该进程的终端相绑定的;但守护进程能突破这种限制,它脱离终端并且在后台运行,脱离终端的 目的是为了避免进程在运行的过程中的信息在终端显示并且进程也不会被任何终端所产生的信息
所打断。

编写守护进程的步骤:

1.创建子进程,终止父进程

2.子进程调用setsid创建会话

3.将工作目录改为根目录

4.重设文件权限掩码umask

5.关闭不再需要的文件描述符

6.将文件描述符为0,1,2定位到/dev/null

7.忽略SIGCHLD信号

SIGHUP 信号

当用户准备退出会话时,系统向该会话发出 SIGHUP 信号,会话将 SIGHUP 信号发送给所有子进程, 子进程接收到 SIGHUP 信号后,便会自动终止,当所有会话中的所有进程都退出时,会话也就终止了。因为程序当中一般不会对 SIGHUP 信号进行处理,所以对应的处理方式为系统默认方式, SIGHUP 信号的系 统默认处理方式便是终止进程

12.进程间的通信

简称为IPC,在不同进程之间传递信息或交换信息,进程实现通信,需要借助一个第三方资源,也就是公共资源,这个资源不属于任何进程,然后各个进程对公共资源进行读写【第三方资源由内核提供,进程的通信需要内核的参与】

目的:
1.数据传输? ? ?

2.资源共享

3.通知事件

4.进程控制

进程间的通信机制:

1.管道【把一个进程连接到另外一个进程的数据流】

匿名管道

匿名管道用于父子进程间或者具有“血缘关系”的进程之间的通信

管道是一种单向通信方法,一个进程向管道发送信息,另一个进程从管道读取数据,管道创建之后,就需要确认通信双发的角色,谁作为发送放,谁作为接收方。【半双工】

访问管道跟访问文件一样,使用read读取管道的数据,使用write向管道写入数据,注意的是,文件不存在于磁盘中,管道中的数据是存放在内存中的,所以读写管道并不会访问磁盘,每个管道产生两个文件描述符,一个读管道,一个写管道

#include <unistd.h>

int pipe(int pipefd[2]);     一个读,一个写

pipefd[1]   写
pipefd[0]   读


成功返回0,失败返回-1



先调用pipe创建匿名管道,然后fork子进程,子进程继承了两个文件描述符,子进程和父进程就实现了文件共享,实现进程间的通信

父读子写? ? ? ? ? ? ? ? ? ? 子读父写

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
        int piped[2] = {0};
        if(-1 == pipe(piped))
        {
                perror("pipe error");
                exit(0);
        }
        switch (fork())
        {
                case -1:
                        perror("error");
                        exit(-1);
                case 0:
                        {
                        char redbuf[64] = {0};
                        close(piped[1]);

                        while (1)
                        {
                                read(piped[0],redbuf,sizeof(redbuf));
                                printf("%s\n",redbuf);
                                memset(redbuf,0x0,sizeof(redbuf));
                        }
                        _exit(0);
                        }
                default:
                        close(piped[0]);
                        while(1)
                        {
                                write(piped[1],"hello world", 11);
                                sleep(1);
                        }
                        break;
                        

        }
}

特点:

管道内部有同步,互斥机制

管道的生命周期随着进程的方式终止而终止,因为管道的本质上是通过文件的方式进行访问的

管道提供的是字节流服务,向管道写入数据或者从管道读取数据的字节大小是任意的,只要不超过管道的容量,管道的大小通常是4K,并且管道的数据没有什么格式

管道是单向传输方式,如果实现双向传输,我们可以创建两个管道

管道只在父子进程或者具有亲缘关系的进程通信

读取规则:

读特性:
? ? 写端存在:
? ? ? ? ? ? 管道有数据:返回读到的字节数
? ? ? ? ? ? 管道无数据:程序阻塞
? ??
? ? 写段不存在:
? ? ? ? ? ? 管道有数据:返回读到的字节数
? ? ? ? ? ? 管道无数据:返回0
? ? ? ? ? ??
写特性:
? ? 读端存在:
? ? ? ? ? ? 管道有空间:返回写入的字节数
? ? ? ? ? ? 管道无空间:程序阻塞,直到有空间为止
? ? ? ? ? ??
? ? 读端不存在:
? ? ? ? ? ? 无论管道是否有空间,管道破裂,管道破裂进程终止

命名管道

命名管道是有名字的,在进行进程间的通信,需要创建这个管道文件,命名管道的文件存在文件系统中,管道文件有自己的名字,只要对这个管道文件进行读写文件,就可以向管道中写入数据或者从管道读取数据。所以,命名管道可以作为同一台主机的任意进程进行通信

信号

信号进行进程间的通信,通知进程发生什么事情

内存映射

内存映射就是将文件映射到进程的地址空间,然后直接通过读写地址的方式去访问这个文件的内容

消息队列

多个进程可以将信息写入一个队列,也可以被多个进程接收信息,写入的信息放入在队列尾部,读取信息从队列的头部【数据结构的队列】

信号量

信号量是一个计数器,主要用于控制多个进程或一个进程内的多个线程间对共享资源的访问,相当于内存中的标志

共享内存

多个进程都可以访问同一块内存区域,这块内存区域会映射到各个进程的地址空间,这些进程都可以访问这块内存区域,实现进程间的通信,需要程序中去处理同步,互斥的问题

套接字(socket)

主要用于不同主机间 的进程间的通信,借助网络进行通信

文章来源:https://blog.csdn.net/weixin_63032791/article/details/134961171
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。