【并发程序设计】总篇集 Linux下 C语言 实现并发程序设计

11_Concurrent_Programing

文章目录

1.进程概念

在Linux中,进程是操作系统分配资源和调度运行的基本单位

Linux中的进程有以下用处:

  1. 提高CPU利用率:通过进程的并发执行,可以让多个程序同时利用计算机的资源,这样每个用户都可以感觉自己独占CPU,从而提高了CPU的效率。
  2. 缩短响应时间:多进程环境下,用户可以启动多个任务而不互相干扰,系统能够快速响应用户的指令,从而缩短系统的响应时间。
  3. 资源分配单位:进程是操作系统中最小的资源分配单位,它包含了程序执行过程中所需的所有资源,如地址空间、文件描述符等。
  4. 提供抽象和管理:对于操作系统来说,进程是一种抽象,它允许操作系统通过管理进程来提高整个系统的资源利用率和效率。
  5. 进程间通信:进程可以通过系统调用或信号来进行通信和同步,例如创建子进程、共享内存、管道、消息队列等方式。
  6. 状态转换:进程的状态包括运行态、就绪态、阻塞态等,这些状态的转换由操作系统内核进行管理和调度,保证了系统的稳定运行。
  7. 实现多任务:在Linux系统中,进程使得多任务成为可能。用户可以根据需要运行多个程序,而这些程序可以独立地执行,互不干扰。

进程在Linux系统中扮演着至关重要的角色,它们不仅提高了系统的资源利用率,还实现了多任务的并行处理,使得用户可以更加高效地进行工作。

以下是一些关于Linux进程的重要概念:

  1. 进程定义:进程是一个程序的一次执行实例,它拥有一段可执行的程序代码、专用的系统堆栈空间、独立的存储空间以及一个进程控制块(PCB),在Linux中具体实现为task_struct结构。
  2. 进程要素:每个进程都需要有一段程序供其运行,这是进程存在的前提。此外,进程还需要有自己的独立存储空间,用于存放变量和数据等。进程控制块则包含了进程的状态、优先级、资源使用情况等信息。
  3. 多道程序设计:Linux是一个多道程序设计系统,这意味着系统中可以有多个彼此独立的进程同时运行。Linux允许进程在运行时创建额外的线程,每个进程都会有一个自己的程序计数器,用来记录下一个需要被执行的指令。
  4. 守护进程:在Linux系统中,即使用户退出登录,仍然会有一些后台进程在运行,这些进程被称为守护进程(daemon)。它们通常负责系统服务或维护任务,如打印服务、系统日志服务等。
  5. 进程管理:为了有效地管理和追踪所有运行着的进程,操作系统提供了一系列的工具和命令。用户可以查看所有运行中的进程、查看进程消耗的资源、定位个别进程并对其执行操作,如改变进程的优先级、杀死指定进程、限制进程可用的系统资源等。
  6. 命令工具:Linux提供了许多命令来帮助用户高效地掌控进程管理,例如ps命令用于查看进程状态,top命令用于动态监控进程资源占用情况,kill命令用于终止指定进程等

进程内容

进程的内容通常包括以下几个主要部分:

  1. 程序代码:这是定义进程行为的机器语言指令或高级语言语句的集合。
  2. 数据:进程操作的数据,可能包括变量、常量、数据结构等,它们在进程的地址空间内被存储和管理。
  3. 堆栈:用于维护函数调用和局部变量的存储区域。堆用于动态内存分配,栈用于执行函数调用和返回以及局部变量的存储。
  4. 文件描述符:每个进程都有自己的文件描述符集合,它们是访问文件、管道和网络套接字等资源的引用。
  5. 环境变量:进程运行时环境中的一些参数设置,比如PATH、HOME等,它们影响进程的行为和配置。
  6. 信号处理程序:用于处理接收到的信号的程序,例如中断信号、终止信号等。
  7. 寄存器值:记录了进程最后执行的指令位置等信息的硬件寄存器内容。
  8. 进程控制块(PCB):操作系统用来表示和管理进程的一个数据结构
    1. 进程标识PID
    2. 进程用户
    3. 进程状态、优先级
    4. 文件描述符(可储存1024个)
  9. 用户ID和组ID:标识运行该进程的用户和用户组的信息,决定了进程的权限和访问控制。
  10. 资源分配情况:包括内存分配、CPU时间片分配和其他资源的占用情况。
  11. 上下文信息:包含进程的执行环境,比如CPU寄存器的内容、程序计数器、堆栈指针等,以便在调度时能够正确地恢复执行。
  12. 线程信息:如果进程支持多线程,还会包含每个线程的相关信息和线程间的同步机制。
  13. 这些内容共同组成了进程的全部信息,操作系统通过管理这些信息来调度和管理各个进程,确保系统资源的合理利用和系统的稳定运行。

进程类型

  1. 交互进程:在shell下启动。以在前台运行,也可以在后台运行
  2. 批处理进程:和在终端无关,被提交到一个作业队列中以便顺序执行
  3. 守护进程:和终端无关,一直在后台运行

进程状态

  1. 运行态:进程正在运行,或者准备运行
  2. 可中断等待态:进程在等待一个事件的发生或某种系统资源
  3. 不可中断等待态:同上
  4. 停止态:进程被中止,收到信号后可继续运行
  5. 死亡态:已终止的进程,但pcb没有被释放

在这里插入图片描述

2.进程常用命令

进程信息命令

  1. ps 查看系统进程快照
  2. top 查看进程动态信息
  3. pstree 查看进程树,显示进程的层次结构

top 命令

  1. 格式top [options]
  2. 功能:实时显示系统中各个进程的资源占用情况,包括CPU、内存等
  3. 参数
    • options(可选参数)
      • -d:设置刷新间隔时间。
      • -p:监控指定进程ID的进程。
      • -u:监控指定用户的进程。
      • -n:设置刷新次数。
      • -b:以批处理模式运行。
  4. 示例
    • shift+ > 后翻页
    • shift+ < 前翻页
    • top -p 1234监控指定进程ID为1234的进程
    • top -u root监控指定用户为root的进程

进程信息表

在这里插入图片描述

表头含义:

  • F 进程标志,说明进程的权限,常见的标志有两个:

    • 1:进程可以被复制,但是不能被执行;
    • 4:进程使用超级用户权限;
  • S 进程状态,常见的状态有以下几种:

    • -D:不可被唤醒的睡眠状态,通常用于 I/O 情况
    • -R:该进程正在运行
    • -S:该进程处于睡眠状态,可被唤醒
    • -T:停止状态,可能是在后台暂停或进程处于除错状态
    • -W:内存交互状态(从 2.6 内核开始无效)
    • -X:死掉的进程(应该不会出现)
    • -Z:僵尸进程。进程已经中止,但是部分程序还在内存当中
    • -<:高优先级(以下状态在 BSD 格式中出现)
    • -N:低优先级
    • -L:被锁入内存
    • -s:包含子进程
    • -l:多线程(小写 L)
    • -+:位于后台
  • UID: 运行此进程的用户的 ID

  • PID: 进程的 ID

  • PPID: 父进程的 ID

  • C: 该进程的 CPU 使用率,单位是百分比

  • PRI: 进程的优先级,数值越小,该进程的优先级越高,越早被 CPU 执行

  • NI: 进程的优先级,数值越小,该进程越早被执行;

  • ADDR: 该进程在内存的哪个位置;

  • SZ: 该进程占用多大内存;

  • WCHAN: 该进程是否运行。"-"代表正在运行;

  • TTY: 该进程由哪个终端产生;

  • TIME: 该进程占用 CPU 的运算时间,注意不是系统时间;

  • CMD: 产生此进程的命令名;

进程优先级命令

  1. nice 启动进程时调整进程的优先级
  2. renice 修改已经运行的进程的优先级

nice 命令

  1. 格式nice [options] [command]
  2. 功能启动进程时调整进程的优先级,使得CPU资源分配更加合理
  3. 参数
    • options(可选参数)
      • -n:设置进程的优先级,范围为-20(最高优先级)到19(最低优先级),默认值为0。
      • --adjust=N:调整当前进程的优先级,N的取值范围与-n相同。
    • command:命令,如lscd
  4. 示例
    • nice command以默认优先级运行命令
    • nice -n -20 ls将进程优先级设置为最高(-20)

renice 命令

  1. 格式renice [options] [NI值] -p [ID]
  2. 功能:修改已经运行的进程的优先级
  3. 参数
    • -n :设置进程的优先级,范围为-20(最高优先级)到19(最低优先级),默认值为0。
    • -p:指定要修改优先级的进程ID。
    • -g:指定要修改优先级的进程组ID。
    • -u :指定要修改优先级的用户ID。
  4. 注意:
    • NI 范围是 -20~19。数值越大优先级越低
    • 普通用户调整 NI 值的范围是 0~19,而且只能调整自己的进程。
    • 普通用户只能调高 NI 值,而不能降低。如原本 NI 值为 0,则只能调整为大于 0。
    • 只有 root 用户才能设定进程 NI 值为负值,而且可以调整任何用户的进程。
  5. 示例
    • sudo renice -n -20 -p 1234将进程ID为1234的进程优先级设置为最高(-20)
    • sudo renice +5 -p 1234将进程ID为1234的进程优先级增加5

后台进程命令

  1. jobs 查看后台进程
  2. bg [进程号] 将挂起的进程在后台运行
  3. fg [进程号] 把后台运行的进程放到前台运行
  4. ctrl+z 把刚运行的程序转到后台运行
  5. &: 在命令后面加上 & 符号可以将该命令放到后台执行

3.子进程

在Linux中,子进程是由父进程创建的进程。当一个进程被创建时,它会自动成为一个新进程的父进程,而新进程则成为子进程。子进程可以通过fork()系统调用来创建。

  1. 创建子进程的意义:子进程常用于实现多任务并行处理,提高程序的执行效率。它们可以分担父进程的工作负载,或者执行不同的任务。
  2. 父子进程区别:虽然子进程是父进程的一个拷贝,但它们有不同的内存地址空间。子进程获得与父进程相同的数据和属性的副本,但是有自己的数据段和堆栈段。

创建子进程

fork 函数

  1. 原型

    #include <unistd.h>
    pid_t fork(void);
    
  2. 功能:创建一个新进程,新进程是当前进程的一个副本。新进程从父进程处继承了代码、数据、堆栈等资源,但是它们在不同的内存空间中运行。

  3. 参数:无

  4. 返回值

    • 成功,创建新进程,则在父进程中返回新进程的进程ID(大于0),在子进程中返回0。
    • 失败,返回-1。
  5. 示例1:在父进程中打印"pid = 进程号",在子进程中打印"pid = 0"

    #include <stdio.h>
    #include <unistd.h>
    int main(int argc, const char *argv[])
    {
          
          
    	pid_t pid;
    	pid = fork();
    	printf("pid = %d\n",pid);
    	return 0;
    }
    

    示例2:fork的一般用法

    #include <stdio.h>
    #include <unistd.h>
    int main(int argc, const char *argv[])
    {
          
          
    	pid_t pid;
    	pid = fork();
    	printf("pid = %d\n",pid);
    
    	if(pid > 0)//父进程
    	{
          
          
    		printf("Father\n");
    	}
    	else if(pid == 0)//子进程
    	{
          
          
    		printf("Child\n");
    	}
    	else//出错
    	{
          
          
    		perror("fork\n");
    		return 0;
    	}
    	return 0;
    }
    

    示例3:使用for循环生成多个子进程

    #include <stdio.h>
    #include <unistd.h>
    int main(int argc, const char *argv[])
    {
          
          
    	pid_t pid;
    	int i;
    	for(i=0; i<3; i++)
    	{
          
          
    		pid = fork();
    		if(pid < 0)
    		{
          
          
    			perror("fork");
    			return 0;
    		}
    		else if(pid == 0)
    	 	{
          
          
    			printf("%d Child %d\n",i,pid);
    			sleep(5);
    		}
    		else if(pid > 0)
    		{
          
          
    			printf("%d Father%d\n",i,pid);
    			sleep(5);
    		}
    	}
    	return 0;
    }
    

    示例3分析

    生成3个子进程3个孙进程1个曾孙进程

    • 子进程:2、3、5 (1 生的)
    • 孙进程:4、6、7 (2、3、5 生的)
    • 曾孙进程:8 (4、6、7 生的)

    在这里插入图片描述

  6. 注意

    1. 子进程只执行fork之后的代码
    2. 父子进程执行顺序是操作系统决定的
    3. 子进程继承了父进程的内容
    4. 父子进程有独立的地址空间,互不影响
    5. 若父进程先结束,子进程成为孤儿进程,被init进程收养,子进程变成后台进程
    6. 若子进程先结束,父进程如果没有及时回收,子进程变成僵尸进程

结束进程

exit 函数

  1. 原型

    #include <unistd.h>
    void exit(int status);
    
  2. 功能:终止当前进程,刷新(流)缓冲区并将退出状态码status传递给操作系统。这个状态码可以被其他进程或父进程获取,以了解子进程的结束状态

  3. 参数status是一个整数,表示进程的退出状态码

  4. 返回值:无

_exit 函数

  1. 原型

    #include <unistd.h>
    void _exit(int status);
    
  2. 功能:与exit函数类似,但_exit函数不会刷新(流)缓冲区,它直接终止进程,并将退出状态码传递给操作系统。

  3. 参数status是一个整数,表示进程的退出状态码

  4. 返回值:无

return 和 exit 的区别:
​ main函数结束时会隐式地调用exit函数,普通函数return是返回上一级。

  1. 示例:

    #include <stdio.h>
    #include <stdlib.h>
    int main(void) 
    {
          
          
        printf(“before exit”);
        exit(0);
        printf(“after exit”);
    }
    

    编译运行后只打印“before exit”

回收子进程

回收子进程的必要性:

  1. 避免资源浪费:当一个子进程结束时,它能够释放自己用户区的资源,但无法释放内核空间的资源,如进程控制块(PCB)。如果父进程不回收这些资源,它们将一直占用内存,导致系统资源的浪费。
  2. 获取退出信息:父进程通过回收子进程可以获取到子进程的退出状态,如退出码和执行时间等信息。这对于父进程监控子进程的行为和进行后续处理是非常重要的。
  3. 防止僵尸进程:如果父进程没有及时回收子进程,那么子进程虽然已经结束,但其PCB仍然保留在系统中,这样的进程称为僵尸进程。僵尸进程不执行任何操作,但占用系统资源,如果大量僵尸进程存在,会影响系统性能。
  4. 管理孤儿进程:如果子进程的父进程先于子进程结束,子进程将成为孤儿进程。为了避免孤儿进程无人管理,系统会让init进程(进程号为1的进程)收养它们。init进程会负责回收这些孤儿进程的资源。

回收子进程不仅是为了维护操作系统资源的有效性,也是为了保证程序能够正确获取子进程的执行结果,以及防止产生僵尸进程和妥善管理孤儿进程,从而保证系统的稳定性和程序的可靠性。

wait 函数

  1. 原型

    #include <unistd.h>
    pid_t wait(int *status);
    
  2. 功能:挂起父进程的执行,直到一个子进程结束。一旦有子进程结束,wait()函数将返回该子进程的PID,并回收其资源。如果传入了status参数,则wait()函数还会将子进程的退出状态码写入到status指向的变量中。

  3. 参数status是一个整数指针,用于存储子进程的退出状态码。

  4. 返回值

    • 成功,返回已结束的子进程的PID;
    • 失败,返回-1,并设置errno为相应的错误码
    • 若子进程没有结束,父进程一直阻塞
    • 若有多个子进程,哪个先结束就先回收
  5. 示例

    #include <stdio.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdlib.h>
                         
    int main(int argc, char** argv)
    {
          
          
       pid_t pid;
       pid_t rpid;
       pid = fork();
       int status;
       if(pid<0)//出错
       {
          
          
          perror("fork");
          return 0;
       }
       else if(pid == 0)//子进程
       {
          
          
           sleep(2);
           printf("child 2 \n");
           exit(2);
       }
       else if(pid >0)//父进程
       {
          
          
           rpid = wait(&status);
           printf("Get child status=%d\n",WEXITSTATUS(status));
       }
    }
    

waitpid 函数

  1. 原型

    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *status, int options);
    
  2. 功能:等待指定子进程的状态改变。当子进程结束时,该函数会返回子进程的进程ID,并将子进程的退出状态存储在status指向的变量中

  3. 参数

    • pid:指定要等待的子进程的进程ID。可以是以下值的组合:
      • -1:等待任意子进程。
      • 0:等待与调用进程属于同一进程组的任何子进程。
      • >0:等待指定的子进程。
    • status:指向一个整数变量的指针,用于存储子进程的退出状态。如果不关心退出状态,可以设置为NULL
    • options:指定额外的选项,可以是以下值的组合:
      • WNOHANG:如果没有子进程状态改变,立即返回,不阻塞。
      • WUNTRACED:如果子进程进入暂停状态,也视为状态改变。
  4. 返回值

    • 成功时,返回已经停止的子进程的进程ID。
    • 如果没有任何子进程状态改变,根据options参数的设置,可能返回0或-1。
    • 失败时,返回-1,并设置errno。
  5. 示例

    #include <stdio.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdlib.h>
                         
    int main(int argc, char** argv)
    {
          
          
       pid_t pid;
       pid_t rpid;
       pid = fork();
       int status;
       if(pid<0)//出错
       {
          
          
          perror("fork");
          return 0;
       }
       else if(pid == 0)//子进程
       {
          
          
           sleep(10);
           printf("child 2 \n");
           exit(2);
       }
       else if(pid >0)//父进程
       {
          
          
           waitpid(pid,&status,0);
           printf("Get child status=%d\n",WEXITSTATUS(status));
       }
    }
    

读取status参数

当使用wait()waitpid()函数等待子进程结束时,可以通过传入的int *status指针来获取子进程的退出状态。这个状态包含两个主要部分:退出代码和信号编号(如果适用)。

通过以下宏来获取status的信息

  • WIFEXITED(status) 判断子进程是否正常结束
  • WEXITSTATUS(status) 获取子进程返回值
  • WIFSIGNALED(status) 判断子进程是否被信号结束
  • WTERMSIG(status) 获取结束子进程的信号类型

示例-创建进程链

创建一个进程链,父进程->子进程->孙进程->重孙进程->重重孙进程

#include <stdio.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
    
    
	pid_t pid;
	int i;

	for(i=0; i<5; i++)
	{
    
    
		pid=fork();
		if(pid < 0)
		{
    
    
			perror("fork");
			return 0;
		}
		if(pid > 0)
		{
    
    
			printf("father %d\n",i);
			break;
		}
		if(pid == 0)
		{
    
    
			printf("child %d\n",i);
		}
	}
	sleep(20);
	wait(0);
	return 0;
}

通过ps -elf | grep a.out 指令查看

在这里插入图片描述

4.exec函数族

exec函数族是一组用于在进程中启动另一个程序来替换当前进程的函数

exec函数族主要用于在当前进程内部执行一个新的程序,而不会创建新的进程。

  • 子进程调用exec函数,族父进程不受影响。
  • 进程当前内容被指定的程序替换,但进程号不变

exec函数族中的一些主要成员:

  1. execl:这是exec函数族中最简单的一个,它接受一个可执行文件路径和一个参数列表,列表以NULL结尾。这个函数将新程序的参数一一对应地传递给新程序。
  2. execv:与execl类似,但是参数是通过一个指针数组传递的,而不是直接列出。
  3. execlpexecvp:这两个函数会搜索PATH环境变量来找到可执行文件,而不需要提供完整的路径名。

execl 函数

  1. 原型

    #include <unistd.h>
    int execl(const char *path, const char *arg, ...);
    
  2. 功能execl函数用来执行一个文件,并将控制权转交给这个新程序。当调用成功时,原有进程的内容(代码段、数据段和堆栈等)将被新程序的内容取代,execl函数不会返回。

  3. 参数

    • path:要执行的程序的路径。
    • arg:传递给新程序的参数列表,最后一个参数需要是NULL,以标识参数列表的结束。
  4. 返回值

    • 如果调用成功,execl函数不会返回。
    • 如果调用失败(例如找不到指定的文件或没有足够的内存等),则返回-1,并设置errno来指示错误类型。
  5. execl函数后面的字母“l”代表的是“list”,意味着该函数通过参数列表的方式来传递参数

  6. 示例:

    使用execl函数来执行ls命令

    #include <stdio.h>
    #include <unistd.h>
    int main(int argc, const char *argv[])
    {
           
           
    	if(execl("/bin/ls","ls","-a","-l","./",NULL)<0)
    	{
           
           
    		perror("execl");
    	}
    	return 0;
    }
    

    编译运行a.out 和 shell命令$ls -a -l 做对比

    在这里插入图片描述

    运行效果一致

execlp 函数

  1. 原型

    #include <unistd.h>
    int execlp(const char *file, const char *arg, ...);
    
  2. 功能execlp函数用于在当前进程中执行指定的可执行文件,并用该新程序替换当前进程的映像。

  3. 参数

    • file:要执行的程序的文件名(或路径),不需要带扩展名,因为系统会自动根据文件名查找可执行文件。
    • arg:传递给新程序的参数列表,最后一个参数需要是NULL,以标识参数列表的结束。
    • ...:可选参数,可以传递多个,每个参数都会按顺序传递给新程序。
  4. 返回值

    • 如果调用成功,execlp函数不会返回。
    • 如果调用失败,则返回-1,并设置errno来指示错误类型。

execv 函数

  1. 原型

    #include <unistd.h>
    int execv(const char *path, char *const argv[]);
    
  2. 功能execv函数用来执行一个文件,并将控制权转交给这个新程序。与execl相似,当调用成功时,原有进程的内容(代码段、数据段和堆栈等)将被新程序的内容取代,execv函数不会返回。

  3. 参数

    • path:要执行的程序的路径。
    • argv:传递给新程序的参数数组,其中argv[0]通常是被执行文件的路径,数组的最后一个元素应该是NULL,以标识参数列表的结束。
  4. 返回值

    • 如果调用成功,函数不会返回。
    • 如果调用失败,则返回-1,并设置errno来指示错误类型。

execvp 函数

  1. 原型

    #include <unistd.h>
    int execvp(const char *file, char *const argv[]);
    
  2. 功能execvp函数用于在当前进程中执行指定的可执行文件,并用该新程序替换当前进程的映像。

  3. 参数

    • file:要执行的程序的文件名(或路径),不需要带扩展名,因为系统会自动根据文件名查找可执行文件。
    • argv:传递给新程序的参数数组,其中argv[0]通常是被执行文件的路径,数组的最后一个元素应该是NULL,以标识参数列表的结束。
  4. 返回值

    • 如果调用成功,execvp函数不会返回。
    • 如果调用失败,则返回-1,并设置errno来指示错误类型。
  5. 示例

    使用execvexecvp函数来执行ls命令

    #include <stdio.h>
    #include <unistd.h>
    int main(int argc, const char *argv[])
    {
          
          
    char *arg[] = {
          
          “ls”,-a”,-l”,/etc”, NULL};
    if  (execv(/bin/ls”, arg) < 0) {
          
          
    perror(“execv”);
    }  
    if  (execvp(“ls”, arg) < 0) {
          
          
    perror(“execvp”);
    }  
    	return 0;
    }
    

    运行效果同上

system 函数

  1. 原型

    #include <stdlib.h>
    int system(const char *command);
    
  2. 功能system函数用于在当前进程中创建一个子进程,并在子进程中执行一个shell命令。该函数会等待命令执行完成后返回。

  3. 参数

    • command:指向以空字符终止的字符串的指针,该字符串包含要在子进程中执行的命令。
  4. 返回值:如果system函数成功执行了指定的命令,它将返回命令的退出状态。如果发生错误或命令无法执行,则返回-1。通常,返回值是shell的退出代码,可以通过WEXITSTATUS(status)宏来获取。

  5. 注意system函数的使用可能会带来安全风险,因为它允许执行任意的shell命令。因此,在安全性要求较高的环境中,应该避免使用system函数,或者至少对输入的命令进行严格的检查和限制。

  6. 示例

    #include <studio.h>
    #include <stdlib.h>
    int main()
    {
          
          
        system("ls -a -l ./");
    }
    

5.守护进程

概念:

守护进程又叫精灵进程(Daemon Process),它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

特点:

始终在后台运行,独立于任何终端,周期性的执行某种任务或等待处理特定事件。

它是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭

举例:

http 服务的守护进程叫 httpd,mysql 服务的守护进程叫 mysqld。

相关名词:

  • 进程组(Process Group): 进程集合,每个进程组有一个组长(Leader),其进程 ID 就是该进程组 ID。
  • 会话(Session): 进程组集合,每个会话有一个组长,其进程 ID 就是该会话组 ID。
  • 控制终端(Controlling Terminal):每个会话可以有一个单独的控制终端,与控制终端连接的 Leader 就是控制进程(Controlling Process)。

创建守护进程

新用到的函数有:

  1. setsid 于创建一个新的会话,并将当前进程设置为新会话的组长
  2. getsid 获取当前进程所在会话的会话ID
  3. getpid 获取当前进程的进程ID
  4. getpgid 获取当前进程的进程组ID

setsid 函数

  1. 原型

    #include <unistd.h>
    pid_t setsid(void);
    
  2. 功能:创建一个新的会话,并将当前进程设置为新会话的领导者。

  3. 返回值

    • 成功时返回新的会话ID
    • 失败时返回-1。

getsid 函数

  1. 原型

    #include <unistd.h>
    pid_t getsid(pid_t pid);
    
  2. 功能:获取指定进程所在的会话ID。

  3. 参数pid - 要查询的进程ID。如果传入0,则返回当前进程所在的会话ID。

  4. 返回值

    • 成功时返回指定进程所在的会话ID
    • 失败时返回-1。

getpid 函数

  • pid_t getpid(void);

  • 获取进程id

getpgid 函数

  • pid_t getpgid(pid_t pid);

  • 获取进程组id

组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。

创建步骤

1.创建子进程,父进程退出

创建子进程然后结束父进程,子进程变成孤儿进程,被init进程收养,子进程在后台运行

if (fork() > 0)  
{
    
    
exit(0);
}

查看进程: p s − e f ∣ g r e p a . o u t 结束进程: ps -ef|grep a.out 结束进程: psefgrepa.out结束进程:kill -9 [进程号]

更简便的创建后台进程(不建议使用)

通过这条命令运行代码:$ nohub ./a.out &
进程进入后台运行

2.子进程创建新会话

if (setsid() < 0)  
{
    
    
	exit(-1);
}

子进程成为新的会话组长
子进程脱离原先的终端

4.更改当前工作目录

chdir(/);//更改当前工作目录到根目录

守护进程一直在后台运行,其工作目录不能被卸载
重新设定当前工作目录cwd

5.重设文件权限掩码

if (umask(0) < 0)  
{
    
    
	exit(-1);
}

文件权限掩码设置为0
只影响当前进程

6.关闭打开的文件描述符

close(0);
close(1);
close(2);

关闭所有从父进程继承的打开文件
已脱离终端,stdin / stdout / stderr无法再使用

7.守护进程创建完成

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(int argc, const char *argv[])
{
   
   
  pid_t pid;
  pid = fork();
  if(pid<0)
  {
   
   
    perror("fork");
    return 0;
  }
    
  //1.结束父进程
  else if(pid>0)
  {
   
   
    exit(0);
  }
  printf("Deamon\n");
  printf("sid=%d,pid=%d,pgid=%d\n",
         getsid(getpid()),
         getpid(),
         getpgid(getpid()));
    
  //2.子进程创建新会话
  if(setsid()<0)
  {
   
   
    perror("setsid");
    exit(0);
  }
  printf("after sid=%d,pid=%d,pgid=%d\n",
         getsid(getpid()),
         getpid(),
         getpgid(getpid()));
    
  //3.更改当前工作目录到根目录
  chdir("/");
  
  //4.文件权限掩码设置为0
  if(umask(0)<0)
  {
   
   
    perror("unmask");
    exit(0);
  }
    
  //5.关闭打开的文件描述符
  close(0);
  close(1);
  close(2);
  printf("after close \n");  
  
  sleep(100);
  return 0}

GDB调试多进程程序

gcc编译.c程序时加入 -g 参数

gcc -g xxx.c
gdb a.out #进入调试

在这里插入图片描述

出现上图表示进入了gdb调试

gdb相关命令

  • run 全速运行
  • star 单步调试
  • n 下一步
  • set follow-fork-mode child/parent 设置GDB只跟踪 子/父 进程代码
  • set setach-on-fork on/off 设置GDB跟踪调试单个进程或多个(默认为on)
    • on: 只调试父进程或子进程的其中一个,(根据follow-fork-mode来决定),这是默认的模式
    • off:父子进程都在gdb的控制之下,其中一个进程正常调试(根据follow-fork-mode来决定),另一个进程会被设置为暂停状态。
  • info inferiors 显示GDB调试的进程
  • inferiors [进程序号] (1,2,3....)切换GDB调试的进程

6.线程创建

线程概念

线程和进程在使用资源、创建开销以及通信方式上存在显著差异

首先,进程是系统资源分配的独立单位,每个进程拥有自己的地址空间,而线程则共享所隶属进程的地址空间。这意味着不同进程之间的资源如内存堆、栈是不能直接共享的,而同一进程内的多个线程可以直接访问这些共享资源。

其次,进程相较于线程有更大的创建和管理开销。因为进程有独立的地址空间,所以操作系统在创建或销毁进程时需要较大的系统资源开销。而线程作为调度的基本单位,其创建和上下文切换的开销要小得多,这也是为什么线程被称为轻量级进程的原因。

最后,由于线程间共享内存空间,它们之间的通信和数据共享更为简单直接。进程间则通常需要借助于进程间通信(IPC)机制,如管道、消息队列、共享内存等来完成数据交换。

线程特点

  • 通常线程指的是共享相同地址空间的多个任务
  • 使用多线程的好处
    • 大大提高了任务切换的效率
    • 避免了额外的TLB & cache的刷新

线程共享资源

一个进程中的多个线程共享以下资源:

  1. 可执行的指令
  2. 静态数据
  3. 进程中打开的文件描述符
  4. 当前工作目录
  5. 用户ID
  6. 用户组ID

线程私有资源

  1. 线程ID (TID)
  2. PC(程序计数器)和相关寄存器
  3. 堆栈
  4. 错误号 (errno)
  5. 优先级
  6. 执行状态和属性

线程创建

pthread_create 函数

  1. 原型

    #include <pthread.h>
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
    
  2. 功能:创建一个新的线程,并使其开始执行指定的函数。

  3. 参数

    • pthread_t *thread:指向一个pthread_t类型的指针,用于存储新创建线程的标识符。
    • const pthread_attr_t *attr:指向一个pthread_attr_t类型的指针,用于设置线程的属性。如果传入NULL,则使用默认属性。
    • void *(*start_routine) (void *):指向一个函数指针,该函数将在新线程中执行。函数的返回类型为void *,参数为void *
    • void *arg:传递给start_routine函数的参数。
  4. 返回值

    • 成功时返回0
    • 失败时返回错误码。
  5. 注意:创建线程后,若主进程运行结束,它创建的线程也会随之结束。

    所以,要在主进程预留一段时间等待线程结束,避免线程中断。

错误解决

使用pthread_create函数时出现的错误:

test_createP.c:(.text+0x4b):对‘pthread_create’未定义的引用
collect2: error: ld returned 1 exit status

这个为链接错误,因为pthread_create函数的库为动态链接库
解决:在编译时加上 -lpthread
例:$gcc test.c -lpthread

线程结束

pthread_exit 函数

  1. 原型

    #include <pthread.h>
    void pthread_exit(void *retval);
    
  2. 功能:结束当前线程,线程私有资源被释放,并将返回值传递给其他线程

  3. 参数void *retval:指向一个指针,用于存储线程的返回值。该值可以被其他线程通过pthread_join()函数获取。

查看线程

pthread_self 函数

  1. 原型

    #include <pthread.h>
    pthread_t pthread_self(void);
    
  2. 功能:获取调用线程(自身所处线程)的标识符。

  3. 返回值:返回调用线程的标识符,类型为pthread_t

线程传参

线程间传参有两种方式,值传递地址传递

示例:arg1为地址传递,arg2为值传递

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int * test_pth1(void *arg)
{
    
    
	printf("p1 tid = %lu\n",pthread_self());//线程ID
	printf("p1 pid = %d\n",getpid());//进程ID
	printf("p1 arg = %d\n",*(int *)arg);//传的参数
	pthread_exit(NULL);//退出线程
}
int * test_pth2(void *arg)
{
    
    
	printf("p2 tid = %lu\n",pthread_self());//线程ID
	printf("p2 pid = %d\n",getpid());//进程ID
	printf("p2 arg = %d\n",(int)arg);//传的参数
	pthread_exit(NULL);//退出线程
}
int main(int argc, const char *argv[])
{
    
    
	pthread_t tid;//线程ID
	int ret;
	int arg1,arg2;
	/*创建线程*/

	for(arg1=0; arg1<4; arg1++)
	{
    
    //地址传递
		ret = pthread_create(&tid,NULL,(void *)test_pth1,(void *)&arg1);
	}
sleep(1);
	for(arg2=0; arg2<4; arg2++)
	{
    
    //值传递
		ret = pthread_create(&tid,NULL,(void *)test_pth2,(void *)arg2);
	}
	printf("main tid%lu\n",tid);
	sleep(1);//等待线程结束
	return 0;
}

运行结果:

在这里插入图片描述

地址传递arg的值都为4,而值传递的arg值为创建线程时传入的值

地址传递的值会随着地址指向的数改变而改变,而线程创建的速度要比线程运行的速度要快,四个线程创建完后arg 的值为4,然后线程才开始运行,所以都打印出arg=4。


7.线程回收

两种方法回收线程

  1. 使用pthread_join 函数
  2. 使线程分离

1.pthread_join 函数

  1. 原型

    #include <pthread.h>
    int pthread_join(pthread_t thread, void **retval);
    
  2. 功能:等待一个线程的结束

  3. 参数

    • thread:需要等待的线程ID。
    • retval:指向一个指针,用于存储被等待线程的返回值。如果不关心返回值,可以设置为NULL。
  4. 返回值

    • 成功时,返回0;
    • 失败时,返回一个非零的错误码。
  5. 注意pthread_join 是阻塞函数,如果回收的线程没有结束,则一直等待

  6. 示例

    创建了一个子线程,并在子线程中执行一个函数fun。主线程等待子线程结束后,打印子线程的返回值。

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    void *fun(void * arg)
    {
           
           
    	int i;
    	pthread_detach(pthread_self(<
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值