Linux之进程与线程详解(一文足矣)

1.进程与线程主要区别

文章后面也有区别的讲解,这里做一个大体了解,有个概念性的认识

Linux 进程和线程都是计算机执行程序的基本单位,但是它们有一些区别。下面是它们之间的主要区别:

资源占用

Linux 进程是独立的程序执行实例,拥有自己的地址空间、文件描述符、进程ID等系统资源,因此会占用更多的系统资源。而线程是进程内部的执行流,它们共享进程的地址空间和其他系统资源,因此占用的资源要比进程少得多。

调度

Linux 进程由调度程序调度执行,进程之间需要进行上下文切换,这会带来一定的开销。而线程是由内核的调度程序在进程内部调度执行的,因此不需要进行上下文切换,线程的切换开销比进程要小得多。

通信和同步

Linux 进程之间的通信和同步需要使用进程间通信机制(如管道、信号、共享内存等),因为它们之间是相互独立的。而线程之间可以直接共享进程的地址空间,因此它们之间的通信和同步要比进程更加高效。

安全性

由于每个进程都有自己独立的地址空间和系统资源,因此进程之间相互隔离,一个进程的错误不会影响其他进程。而线程共享进程的地址空间和资源,因此一个线程的错误可能会影响整个进程。例如,如果一个线程写入了一个错误的地址,会导致整个进程崩溃。因此,在使用线程时需要特别注意线程之间的数据访问和同步,以避免线程安全问题。

另外,由于线程共享进程的地址空间,因此线程之间的调用比进程更加快速,但同时也需要更加小心,以避免一个线程修改了另一个线程正在使用的数据。因此,线程编程需要注意避免数据竞争和锁定问题。

总的来说,安全性是进程和线程之间的一个重要区别,进程的隔离性更好,但是线程的通信和执行效率更高,开发者需要根据具体需求来选择使用进程或线程。

灵活性

进程比线程更加灵活。由于每个进程都是独立的程序实体,因此可以独立地运行、停止、调试和管理。而线程是在进程内部执行的,它们共享进程的资源,因此线程之间的状态和行为更加紧密相关。

创建和销毁

创建和销毁线程比进程要快得多。这是因为线程是在进程内部创建的,它们共享进程的资源,因此创建和销毁线程的开销要比创建和销毁进程小得多。另外,线程的创建和销毁也不需要像进程那样需要重新分配和释放系统资源,因此开销更小。

调试

在调试时,进程比线程更容易处理。由于进程是相互独立的,因此可以分别调试每个进程,而线程共享进程的地址空间和资源,因此在调试线程时,需要特别注意线程之间的交互和影响,这使得调试线程变得更加困难。

可扩展性

由于线程共享进程的资源,因此它们比进程更容易扩展。在多处理器系统中,多线程程序可以更好地利用系统资源,从而提高程序的性能和可扩展性。而进程之间的通信和同步需要使用额外的系统资源,因此在多处理器系统中,进程之间的通信和同步可能会成为瓶颈,限制系统的可扩展性。

2 进程函数用法及讲解

2.1 进程的创建和终止

2.1.1 fork

#include <sys/types.h>
#include <unistd.h>

       pid_t fork(void);

fork的返回值问题:

在父进程中,fork返回新创建子进程的进程ID;

在子进程中,fork返回0;

如果出现错误,fork返回一个负值;  

getppid():得到一个进程的父进程的PID;

getpid():得到当前进程的PID;

*注意:在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

2.1.2 exit 

进程退出一般有三种方法:
1、在main函数中使用 return关键字 ,使用 return 后系统会调用 exit()函数来终止进程。
2、手动调用 exit() 来终止进程。
3、调用 _exit() 来终止进程。

_exit会立刻结束进程将缓存释放掉,而exit先查看当前进程有没有文件缓存区,如果有,会先处理缓存区的数据,然后释放内存,下面这张图是一个很好的解释:

#include <unistd.h>

       void _exit(int status);

#include <stdlib.h>

        void exit(int status);

                exit(0):表示运行正常结束进程;exit(1):表示异常退出,返回值1是给操作系统的

注意return 是关键字,是语言级别,表示调用堆栈的返回,并将控制权移交给控制的前一级;exit是函数,表示系统级别,表示进程的结束。

2.1.3 wait 

#include <sys/types.h>
#include <sys/wait.h>

       pid_t wait(int *wstatus);
函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

返回值:正常情况下,wait的返回值为子进程的PID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD

注:

  当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.

  如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态.

参数解释:

  参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:

pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

  如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:

1,WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。

(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数–指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)

2, WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
 

/*
下面的程序没有调用wait函数,那么子进程会成为无父进程状态,也就是僵尸进程,可以根据程序运行后
所打印的PID,然后命令行输 ps -aux 查看进程状态,
发现这个子进程依然存在,所以要调用wait来进行所谓的收尸
*/
#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
	pid_t pid;//定义fork的返回值,存储在相同类型的变量pid中
//如果创建成功,在父进程中fork返回新创建的子进程的进程号,在子进程中返回0
	printf("hello world\n");
	pid=fork();//fork创建子进程,将返回值给pid
	if(pid<0)//如果出错,fork返回一个负值并设置错误码
		perror("fork error");
	else if(pid==0)//判断pid==0就是子进程
		printf("I am child,my pid:%d\n",getpid());//getpid得到当前进程的进程号,getppid()是得到父进程的进程ID
	else if(pid>0)//如果pid>0是父进程
		printf("I am father,my pid:%d\n",pid);//

	puts("hi ");//这里是父子进程都执行的部分,运行结果可以看到两个hi,
	exit(0);
}
/*
下面的程序调用了wait来为孩子收尸,这样再ps -aux会发现进程结束,没有尸体残留
*/
#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(){
	pid_t pid;//定义fork的返回值,存储在相同类型的变量pid中
	printf("hello world\n");
	pid=fork();//fork创建子进程,将返回值给pid
	if(pid<0)//如果出错,fork返回一个负值并设置错误码
		perror("fork error");
	else if(pid==0){//如果创建成功,在子进程中,返回的pid==0  是子进程
		printf("I am child,my pid:%d\n",getpid());//getpid得到当前进程的进程号,getppid()得到父进程的进程号
		printf("my father's pid:%d\n",getppid());
	}
	else if(pid>0){//如果pid>0是父进程,返回的pid 为创建的子进程的ID
		printf("I am father,my son's pid:%d\n",pid);
		printf("my pid:%d\n",getpid());
		int status;
		pid_t sonpid=wait(NULL);
		if(sonpid<0)puts("调用进程没有子进程");
		else printf("等到了孩子的尸体,孩子进程号为:%d\n",sonpid);

	}
	puts("hi ");//这里是父子进程都执行的部分,运行结果可以看到两个hi,
	exit(0);
}

2.2 exec函数族

exec函数族简介:

        exec函数族提供一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。

使用时机:

1.当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec 函数族让自己重生。

2.如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。子进程执行完新的程序后,它依然需要父进程wait收尸!!!

 注意点:

        exec函数族调用后,正在执行的进程本身的pcb,mm_struct,页表等信息不会发生改变,进程程序替换并不会创建新的进程,它只会加载程序的代码和数据,去替换原来的进程!!

man 3 exec:

NAME
       execl, execlp, execle, execv, execvp, execvpe - execute a file

SYNOPSIS
       #include <unistd.h>

       extern char **environ;

       int execl(const char *pathname, const char *arg, ...
                       /* (char  *) NULL */);
       int execlp(const char *file, const char *arg, ...
                       /* (char  *) NULL */);
       int execle(const char *pathname, const char *arg, ...
                       /*, (char *) NULL, char *const envp[] */);
       int execv(const char *pathname, char *const argv[]);
       int execvp(const char *file, char *const argv[]);
       int execvpe(const char *file, char *const argv[],
                       char *const envp[]);
 

返回值: 
  exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。 
参数说明: 
path:可执行文件的路径名字 
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束 
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助: 
l : 使用参数列表 
p:使用文件名,并从PATH环境进行寻找可执行文件 ,自动搜索环境变量PATH
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。 
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量,表示自己维护环境变量

//下面用execl来简单演示以下

//exec_test.c  中的代码,
#include<stdio.h>
#include <unistd.h>
int main(){
	int i=0;
	while(i<6){
		printf("正在执行exec_test.out,此时i:%d\n",i++);
		sleep(1);
	}
	return 0;
}
//exec.c  文件
#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
 #include <unistd.h>
int main(){
	printf("这是刚开始的父进程,进程号:%d\n",getpid());
	pid_t pid=fork();
	if(pid<0){
		perror("fork error");
	}
	else if(pid==0){
		puts("接下来执行子进程");
		int ret=execl("./exec_test.out","exec_test.out",NULL,NULL);
		if(ret<0)perror("exec error");
		puts("子进程执行结束");
	}
	else if(pid>0){
		printf("这是父进程,父进程pid:%d\n",getpid());
		printf("我创建的子进程的pid:%d\n",pid);
		puts("接下来调用wait");
		  pid_t sonpid=wait(NULL); 
        if(sonpid<0)puts("调用进程没有子进程"); 
        else printf("等到了孩子的尸体,孩子进程号为:%d\n",sonpid);
	}
puts("准备结束父进程");
return 0;
}

运行结果总结:fork==0的时候,子进程执行,父进程不执行,子进程执行的时候,调用execl,运行另一程序,当运行完之后,不会再去执行父进程程序里剩下的内容,但是该进程依然存在,是一个僵尸进程,此时可以用wait收尸,wait返回值大于0; 

2.2.1 execl 

/*
带l的一类exac函数(l表示list),包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。 
以execl函数为例子来说明
参数:
//可执行文件路径===可执行文件名===参数列表===最后必须以NULL结尾
*/
#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
 #include <unistd.h>
int main(){
	printf("这是刚开始的父进程,进程号:%d\n",getpid());
	pid_t pid=fork();
	if(pid<0){
		perror("fork error");
	}
	else if(pid==0){
		puts("接下来执行子进程");
		int ret=execl("/usr/bin/ls","ls","-a","-l",NULL);
//  /usr/bin/ls 是指运行/usr/bin下的ls程序
		if(ret<0)perror("exec error");
		puts("子进程执行结束");
	}
	else if(pid>0){
		printf("这是父进程,父进程pid:%d\n",getpid());
		printf("我创建的子进程的pid:%d\n",pid);
		puts("接下来调用wait");
		  pid_t sonpid=wait(NULL); 
        if(sonpid<0)puts("调用进程没有子进程"); 
        else printf("等到了孩子的尸体,孩子进程号为:%d\n",sonpid);
	}
puts("准备结束父进程");
return 0;
}

 

2.2.2 execlp

#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
 #include <unistd.h>
int main(){
	printf("这是刚开始的父进程,进程号:%d\n",getpid());
	pid_t pid=fork();
	if(pid<0){
		perror("fork error");
	}
	else if(pid==0){
		puts("接下来执行子进程");
		int ret=execl("ps","ps","-aux",NULL);
		if(ret<0)perror("exec error");
		puts("子进程执行结束");
	}
	else if(pid>0){
		printf("这是父进程,父进程pid:%d\n",getpid());
		printf("我创建的子进程的pid:%d\n",pid);
		puts("接下来调用wait");
		  pid_t sonpid=wait(NULL); 
        if(sonpid<0)puts("调用进程没有子进程"); 
        else printf("等到了孩子的尸体,孩子进程号为:%d\n",sonpid);
	}
puts("准备结束父进程");
return 0;
}

int ret=execlp("ps","ps","-aux",NULL);

2.2.3 execvp

//execvp 只是将参数列表以数组的形式展现,参数为数组指针
char *argv[]={"ls","-l",NULL};
int ret=execvp("ls",argv);

 exec函数族使用的总结
有了exec函数族我们就可以用它来调用任何程序,也就是说,你还可以在C/C++,可以调用其他任何语言,比如python,Java等程序;
这些exec函数的接口函数本质是没有任何差别的,只是参数选项不同罢了;
为什么有那么多种接口,本质满足不同引用场景而已;
这些函数之间的调用关系,本质都会调用系统调用函数execve;

 3. 线程

3.1 什么是线程

线程可以看作是轻量级的进程,Linux线程的本质还是进程,Linux先有进程后有线程,当创建了一个进程时,系统给它分配一段4G的虚拟内存,并生成进程的PCB,当进程使用相关函数创建一个线程时,会为新的线程也生成一个PCB存放在当前的4G虚拟内存中,不需要开辟额外的地址空间,原来的进程也沦为了一个线程(主线程),所以进程和线程的区别就是是否共享地址空间。进程总是独享4G的虚拟内存,而多个线程共享一段4G的空间。

进程是资源分配的最小单位,线程是CPU调度的最小单位,也是CPU分配时间片的单位。因此线程越多的程序获得的CPU的概率也就越大,使用多线程能够提高程序的执行效率。

线程共享的资源:文件描述符;信号的处理方式;当前工作目录;用户ID和组ID;全局变量;虚拟内存地址空间(其实是共享0-3G的空间,除了栈空间和errno变量);

线程独立资源:线程ID,程序计数器和相关寄存器,栈,errno值,信号;其中最重要的是私有栈和寄存器,私有栈是为了保存临时变量,便于函数调用等操作,私有寄存器是为了方便线程切换,保存上下文。

线程的优缺点:

优点:① 提高程序并发性② 开销小③ 数据通信、共享数据方便(不同线程可以使用全局变量)

缺点:① 线程使用第三方库函数,不稳定② 代码不好调试,没法用gdb调试③ 对信号的支持不好 

3.2 进程与线程的区别 

1、进程承担分配系统资源的实体;线程共享进程所获得资源;

2、因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的和代码段、堆栈段数据段,这对于多进程来说十分“奢侈”,系统开销比较大。

而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。(这句话可能有点问题,应该在某种条件下是对的,比如单线程的进程)当一个线程死了(非正常退出、死循环等)就会导致线程该占有的资源永远无法释放,从而影响其他线程的正常工作

(1)体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。

(2)属于同一个进程的所有线程共享该进程的所有资源,包括文件描述符。而不同的进程相互独立。

(3)线程又称为轻量级进程,进程有进程控制块,线程有线程控制块;

(4)线程必定也只能属于一个进程,而进程可以拥有多个线程而且至少拥有一个线程;

(5)体现在程序结构上,举一个简明易懂的列子:当我们使用进程的时候,我们不自主的使用if else嵌套来判断pid,使得程序结构繁琐,但是当我们使用线程的时候,基本上可以甩掉它,当然程序内部执行功能单元需要使用的时候还是要使用,所以线程对程序结构的改善有很大帮助。

3.3 关于线程的函数使用

3.3.1 pthread_create

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg);

函数功能:创建一个线程
函数返回值:成功返回0;失败返回错误码
参数解释:
第一个参数:pthread_t *thread
    pthread_t是类型,在linux下被定义为“unsigned long int”,是一种用于表示线程的数据类型;
    phtread_t *thread:传递一个pthread_t类型的指针变量,也可以传递这个类型变量的地址
第二个参数: const pthread_attr_t *attr
    const pthread_attr_t:该类型以结构体的形式定义在<pthread.h>头文件中,专门表示线程的属性
    const pthread_attr_t *attr:用于手动设置新建线程的属性,例如线程的调用策略、线程所能使用的栈内存的大小等。大部分场景中,我们都不需要手动修改线程的属性,将 attr 参数赋值为 NULL,pthread_create() 函数会采用系统默认的属性值创建线程。

第三个参数:void *(*start_routine)(void *)
    以函数指针的方式指明新建线程需要执行的函数,该函数的参数最多有 1 个(可以省略不写),形参和返回值的类型都必须为 void 类型。void 类型又称空指针类型,表明指针所指数据的类型是未知的。使用此类型指针时,我们通常需要先对其进行强制类型转换,然后才能正常访问指针指向的数据。
    如果该函数有返回值,则线程执行完函数后,函数的返回值可以由 pthread_join() 函数接收
第四个参数:void *arg
    void *arg:指定传递给 start_routine 函数的实参,当不需要传递任何数据时,将 arg 赋值为 NULL 即可。

3.3.2 pthread_exit


#include <pthread.h>

void pthread_exit(void *retval);

功能说明:主动结束当前线程(终止调用它的线程并返回一个指向某个对象的指针)

参数:
 void *retval:函数的返回指针,只要pthread_join中的第二个参数retval不是NULL,这个值将被传递给retval

返回值:无

3.3.3 pthread_join

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

功能说明:
    这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回,可以理解为进程当中的wait收尸

参数:
    pthread_t thread  被等待的线程标识符
    void **retval     一个用户定义的指针,它可以用来存储被等待线程的返回值
返回值:
On success, pthread_join() returns 0; on error,  it  returns  an  error number.
//以下是这三个函数的程序实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void *thread1fun(){
	puts("进入线程1");
	puts("执行线程1");
	puts("hello i am thread1");
	char *p="线程1已经退出,返回指针char *p";
	pthread_exit(p);
}
void *thread2fun(){
	puts("进入线程2");
	puts("执行线程2");
	puts("hello i am thread2");
	char *p="线程2已经退出,返回指针char *p";
	pthread_exit(p);
}
int main(){
	pthread_t thread1;
	pthread_t thread2;//定义两个线程类型变量
	if(pthread_create(&thread1,NULL,thread1fun,NULL)!=0){
		perror("thread1_create error");
		exit(1);

	}
	else {
		puts("线程1创建成功");
		printf("线程1线程号:%lu\n",thread1);
	}

	if(pthread_create(&thread2,NULL,thread2fun,NULL)!=0){
		perror("thread1_create error");
       exit(1);
	}
	else {
		puts("线程2创建成功");
		printf("线程2线程号:%lu\n",thread2);
	}


puts("阻塞等待回收线程");
if(pthread_join(thread1,(void**)&thread1)==0){
	puts("等到了,打印返回指针");
puts((char*) *(&thread1));
}
if(pthread_join(thread2,(void **)&thread2)==0){
	puts("等到了,打印返回指针");

puts((char*) *(&thread2));
}
return 0;
}

 3.3.4 其它线程函数了解

线程取消函数

 #include <pthread.h>

 int pthread_cancel(pthread_t thread);

功能说明:取消某个线程的执行
返回值:成功返回0,否则返回perror

参数:要取消线程的标识符ID

获取当前线程ID函数

#include <pthread.h>

pthread_t pthread_self(void);

功能说明:获取当前调用线程的ID

形参:无

返回值:当前线程的线程ID标识

分离释放线程函数

#include <pthread.h>

 int pthread_detach(pthread_t thread);
功能说明:线程资源释放方式设置函数

形参:要释放线程的标识符ID

返回值:成功返回0,错误返回perror

3.3.5 线程互斥锁的相关函数 

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  1. 参数说明:
  • mutex:指向互斥锁的指针。
  • attr:指向互斥锁属性的指针。可以为 NULL,表示使用默认属性。
  1. 返回值说明:
  • pthread_mutex_init():成功时返回 0,否则返回一个正整数错误码。
  • pthread_mutex_destroy():成功时返回 0,否则返回一个正整数错误码。
  • pthread_mutex_lock():成功时返回 0,否则返回一个正整数错误码。
  • pthread_mutex_unlock():成功时返回 0,否则返回一个正整数错误码。
  1. 功能说明:
  • pthread_mutex_init():用于初始化一个互斥锁。可以使用默认属性或者自定义属性进行初始化。
  • pthread_mutex_destroy():用于销毁一个互斥锁,释放相关资源。
  • pthread_mutex_lock():用于获取一个互斥锁,如果该锁已经被其他线程获取,则当前线程将阻塞,直到该锁被释放为止。
  • pthread_mutex_unlock():用于释放一个互斥锁,如果该锁当前没有被任何线程获取,则此函数将返回一个错误。

在使用线程互斥锁的相关函数时,有一些需要注意的地方:

  1. 互斥锁的初始化和销毁:在使用互斥锁之前,需要先对其进行初始化,使用完毕后需要将其销毁。初始化可以使用 pthread_mutex_init() 函数,销毁可以使用 pthread_mutex_destroy() 函数。

  2. 加锁和解锁的配对:在使用 pthread_mutex_lock()pthread_mutex_unlock() 函数时,需要保证加锁和解锁的配对性,即每次加锁后都必须要对应的解锁。

  3. 避免死锁:死锁是指两个或多个线程互相等待对方释放锁,导致程序无法继续执行。为了避免死锁,需要确保加锁的顺序一致,并且不要在加锁的状态下等待其他锁。

  4. 不要重复加锁:重复加锁会导致线程阻塞,无法进行下去,需要注意避免。

  5. 对共享资源的访问必须要在加锁状态下进行:为了确保多个线程访问共享资源的互斥性,需要在对共享资源进行访问之前,先获取对应的互斥锁,以避免多个线程同时访问共享资源。

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

#define NUM_THREADS 4  // 线程数
#define MAX_COUNT 100  // 共享资源最大值

pthread_mutex_t mutex;  // 定义互斥锁
int count = 0;          // 共享资源

void *increment_counter(void *thread_id) {
    int tid = *((int*)thread_id);  // 获取线程 ID

    while (1) {
        // 加锁,保护共享资源
        pthread_mutex_lock(&mutex);
        
        // 如果共享资源达到最大值,退出循环
        if (count == MAX_COUNT) {
            pthread_mutex_unlock(&mutex);  // 解锁
            break;
        }
        
        // 对共享资源进行操作
        printf("Thread %d: count = %d\n", tid, count);
        count++;
        
        // 解锁
        pthread_mutex_unlock(&mutex);
    }
    
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];  // 线程数组
    int thread_ids[NUM_THREADS];     // 线程 ID 数组
    int i;

    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    
    // 创建多个线程,传递不同的线程 ID
    for (i = 0; i < NUM_THREADS; i++) {
        thread_ids[i] = i;
        pthread_create(&threads[i], NULL, increment_counter, (void*)&thread_ids[i]);
    }

    // 等待所有线程执行完毕
    for (i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

在这个例子中,我们定义了一个全局变量 count,代表共享资源的数量。我们创建了 NUM_THREADS 个线程,每个线程都会通过加锁和解锁来保护共享资源的访问。在每个线程的循环中,我们使用 pthread_mutex_lock() 函数获取互斥锁,使用 pthread_mutex_unlock() 函数释放互斥锁,保证了多个线程之间的互斥访问。

需要注意的是,在本例子中,由于多个线程共享了全局变量 count,因此在对其进行访问时,需要在加锁的状态下进行,以避免多个线程同时访问该变量导致数据不一致的问题。

在使用线程互斥锁时,我们需要注意对共享资源的访问,以及加锁和解锁的配对性等问题,以保证程序的正确性和性能。

3.3.6 线程锁 

上面是互斥锁,但是线程并不只有互斥锁,在实际的多线程编程中,我们应该选择合适的线程锁。

  1. 互斥锁(Mutex):互斥锁是一种二元锁,只有两个状态:锁定和未锁定。当一个线程持有互斥锁时,其他线程必须等待该线程释放锁之后才能继续执行。如果多个线程同时竞争锁,只有一个线程可以获得锁,其他线程将被阻塞,直到获得锁的线程释放锁。互斥锁提供了一种简单而有效的方法,用于确保多个线程之间对共享资源的互斥访问。

  2. 读写锁(Reader-Writer Lock):读写锁是一种更高效的锁,适用于读多写少的场景。读写锁允许多个线程同时读取共享资源,但是只有一个线程可以写入共享资源。当一个线程持有读锁时,其他线程也可以持有读锁,但是当一个线程持有写锁时,其他线程必须等待该线程释放写锁之后才能继续执行。读写锁的优点是在读取共享资源时可以允许多个线程同时进行,从而提高了程序的并发性和性能。

  3. 自旋锁(Spin Lock):自旋锁是一种非阻塞锁,线程在尝试获取锁时不会被挂起,而是一直循环尝试获取锁。当锁被其他线程持有时,线程会一直尝试获取锁,直到锁被释放。自旋锁适用于锁被持有时间很短的场景,因为在等待锁的过程中,线程会消耗大量的CPU资源。

  4. 条件变量(Condition Variable):条件变量用于在某个条件满足时通知等待线程。线程可以在条件变量上等待,直到满足条件后才被唤醒。条件变量通常与互斥锁一起使用,以确保在修改共享资源时不会出现竞态条件。

  5. 信号量(Semaphore):信号量是一种计数器,用于控制多个线程对共享资源的访问。当计数器大于零时,线程可以访问资源,并将计数器减一;当计数器等于零时,线程就必须等待。信号量可以用来实现读写锁和生产者-消费者模型等并发编程模型。

读写锁函数介绍

pthread_rwlock_init()

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

  • rwlock:读写锁对象的指针。
  • attr:读写锁属性对象的指针,可以为NULL。
  • 返回值:成功返回0,失败返回错误码。

此函数用于初始化读写锁对象。读写锁对象在使用前必须被初始化,可以使用静态初始化或者动态初始化。如果使用动态初始化,需要在使用完毕后使用pthread_rwlock_destroy()销毁对象。

pthread_rwlock_rdlock()

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

  • rwlock:读写锁对象的指针。
  • 返回值:成功返回0,失败返回错误码。

此函数用于获取读取锁,如果写入锁已经被占用,则函数会阻塞等待,直到写入锁被释放。多个线程可以同时获取读取锁。

pthread_rwlock_wrlock()

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

  • rwlock:读写锁对象的指针。
  • 返回值:成功返回0,失败返回错误码。

此函数用于获取写入锁,如果读取锁或者写入锁已经被占用,则函数会阻塞等待,直到锁被释放。写入锁是独占的,只有一个线程可以获取写入锁。

pthread_rwlock_unlock()

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

  • rwlock:读写锁对象的指针。
  • 返回值:成功返回0,失败返回错误码。

此函数用于释放读取锁或者写入锁。如果锁没有被获取,释放锁会导致未定义的行为。

pthread_rwlock_tryrdlock()

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

  • rwlock:读写锁对象的指针。
  • 返回值:成功返回0,失败返回错误码。

此函数尝试获取读取锁,如果读取锁已经被占用,则函数会立即返回,不会阻塞等待。如果获取成功,则进行读取操作。

pthread_rwlock_trywrlock()

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

  • rwlock:读写锁对象的指针。
  • 返回值:成功返回0,失败返回错误码。

此函数尝试获取写入锁,如果读取锁或者写入锁已经被占用,则函数会立即返回,不会阻塞等待。如果获取成功,则进行写入操作。

pthread_rwlock_destroy()

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

  • rwlock:读写锁对象的指针。
  • 返回值:成功返回0,失败返回错误码。

此函数用于销毁读写锁

4. 进程通信方式

  1. 管道(Pipe):管道是一种半双工的通信方式,它只能用于父子进程或者兄弟进程之间的通信。管道有两种类型:无名管道和命名管道。无名管道只存在于相关进程的内存中,而命名管道则存在于文件系统中,允许不相关的进程进行通信。

  2. 命名管道(Named Pipe):命名管道允许不相关的进程进行通信。它在文件系统中创建一个特殊文件,任何知道文件名的进程都可以通过文件访问方式进行通信。

  3. 信号(Signal):信号是一种异步通信机制,用于通知进程发生了某些事件。一个进程可以向另一个进程发送信号,另一个进程可以选择接受或忽略信号。

  4. 消息队列(Message Queue):消息队列是一种存放在内核中的消息链表,用于不相关的进程间的通信。进程可以向队列中发送消息,其他进程可以从队列中接收消息。

  5. 共享内存(Shared Memory):共享内存允许多个进程访问同一块物理内存。进程可以将数据写入共享内存区域,其他进程可以直接从该内存区域读取数据。

  6. 套接字(Socket):套接字是一种通用的进程间通信方式,可用于不同主机之间的通信。套接字提供了一组接口,进程可以使用这些接口来建立网络连接并进行通信。

这些是最常见的几种进程间通信方式,不同的场景下可能还有其他的进程间通信方式可供选择。

4.1 共享内存及相关函数详解 

使用共享内存的主要函数有:

  • shmget():创建或打开一个共享内存段。
  • shmat():将一个共享内存段连接到进程的地址空间。
  • shmdt():断开一个共享内存段与进程的连接。
  • shmctl():对一个共享内存段进行控制操作。

shmget()

功能说明:创建或打开一个共享内存段。

原型:

#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数:

  • key:共享内存标识符,可以使用IPC_PRIVATE表示新建一个唯一的共享内存。
  • size:共享内存大小,以字节为单位。
  • shmflg:操作标志位,可以使用IPC_CREAT表示创建新的共享内存,或者使用IPC_EXCL表示如果已存在则报错。还可以指定权限位如0666等。

返回值:

  • 成功时返回一个非负整数,表示共享内存标识符(shmid)。
  • 失败时返回-1,并设置errno。

shmat()

功能说明:将一个共享内存段连接到进程的地址空间。

原型:

#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

参数:

  • shmid:由shmget()返回的共享内存标识符。
  • shmaddr:指定连接到进程地址空间的位置,如果为NULL,则由系统自动分配。
  • shmflg:操作标志位,可以使用SHM_RDONLY表示只读连接,或者0表示可读写连接。

返回值:

  • 成功时返回指向共享内存首地址的指针。
  • 失败时返回(void *) -1,并设置errno。

shmdt()

功能说明:断开一个共享内存段与进程的连接。

原型:

#include <sys/shm.h>
int shmdt(const void *shmaddr);

参数:

  • shmaddr:由shmat()返回的指向共享内存首地址的指针。

返回值:

  • 成功时返回0。
  • 失败时返回-1,并设置errno。

shmctl()

功能说明:对一个共享内存段进行控制操作。

原型:

#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:

  • shmid:由shmget()返回的共享内存标识符。
  • cmd:控制命令,可以使用IPC_STAT表示获取状态信息,IPC_SET表示设置状态信息,IPC_RMID表示删除该共享内存段等。
  • buf:用于传递或接收状态信息的结构体指针。结构体定义如下:
struct shmid_ds {
    struct ipc_perm  shm_perm;    /* Ownership and permissions */
    size_t           shm_segsz;   /* Size of segment (bytes) */
    time_t           shm_atime;   /* Last attach time */
    time_t           shm_dtime;   /* Last detach time */
    time_t           shm_ctime;   /* Last change time */
    pid_t            shm_cpid;    /* PID of creator */
    pid_t            shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    shmatt_t         shm_nattch;  /* No. of current attaches */
};

返回值:

  • 成功时返回0,或者根据cmd的不同返回其他值。
  • 失败时返回-1,并设置errno。

以下程序是一个简单的生产者-消费者模型,其中一个进程向共享内存中写入数据,另一个进程从共享内存中读取数据。请注意,这个程序没有考虑同步和互斥的问题,只是为了演示共享内存的使用方法。

// producer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_KEY 1234 // 共享内存标识符
#define SHM_SIZE 1024 // 共享内存大小

int main()
{
    int shmid; // 共享内存标识符
    char *shmaddr; // 共享内存首地址

    // 创建或打开一个共享内存段
    shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 将共享内存段连接到进程地址空间
    shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (void *) -1) {
        perror("shmat");
        exit(1);
    }

    // 向共享内存中写入数据
    strcpy(shmaddr, "Hello from producer!");

    // 断开与共享内存的连接
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        exit(1);
    }

    printf("Producer done.\n");

    return 0;
}
// consumer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_KEY 1234 // 共享内存标识符
#define SHM_SIZE 1024 // 共享内存大小

int main()
{
    int shmid; // 共享内存标识符
    char *shmaddr; // 共享内存首地址

    // 打开一个已存在的共享内存段
    shmid = shmget(SHM_KEY, SHM_SIZE, 0);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 将共享内存段连接到进程地址空间
    shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (void *) -1) {
        perror("shmat");
        exit(1);
    }

    // 从共享内存中读取数据
    printf("Consumer received: %s\n", shmaddr);

    // 断开与共享内存的连接
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        exit(1);
    }

    // 删除该共享内存段
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }

    printf("Consumer done.\n");

    return 0;
}

文本结束,文章有不足之处请指正,另外有时间会持续更新和完善,期待您的关注和收藏 

  • 14
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Y_寒酥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值