【Linux系统编程学习】Linux线程控制原语

此为牛客Linux C++课程笔记。

0. 关于线程

在这里插入图片描述
注意:LWP号和线程id不同, LWP号是CPU分配时间片的依据,线程id是用于在进程内部区分线程的。

1. 线程与进程的区别

在这里插入图片描述
在这里插入图片描述
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。

但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。
如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。

因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便
缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

2. 线程相关操作函数

在这里插入图片描述

2.1 获取线程id

#include <pthread.h>
pthread_t pthread_self(void);

功能:获取线程ID。其作用对应进程中 getpid() 函数。

2.2 创建线程: pthread_create

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

功能:创建一个子线程

参数:

  • thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
  • attr : 设置线程的属性,一般使用默认属性,即NULL
  • start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
  • arg : 给第三个参数(回调函数)使用,是回调函数的参数

返回值:

  • 成功:0
  • 失败:返回错误号。这个错误号和之前errno不太一样,获取错误号的信息使用:
#include <string.h>
char * strerror(int errnum);

创建线程示例代码如下:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* callback(void* arg) {printf("its child thread, thread id is %lu\n", pthread_self());printf("arg = %d\n", *(int *)arg);
}int main()
{pthread_t pid;int a = 5;int ret = pthread_create(&pid, NULL, callback, &a);if(ret != 0) {// 说明创建失败char * errstr = strerror(ret);printf("error: %s\n", errstr);}printf("its main thread, thread id is %lu\n", pthread_self());sleep(1);return 0;
}

发现无法编译
在这里插入图片描述
查阅文档发现:
在这里插入图片描述
编译时加-pthread即可,运行结果如下:
在这里插入图片描述

2.3 终止线程: pthread_exit

注意,不能使用exit函数终止当前线程,exit将终止当前进程,进程中的所有线程将一并终止。

#include <pthread.h>
void pthread_exit(void *retval);

参数:retval表示线程退出状态,通常传NULL

多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。

2.4 连接已终止的线程(回收线程):pthread_join

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

功能:和一个已经终止的线程进行连接(回收子线程的资源)

注意:这个函数是阻塞函数,调用一次只能回收一个子线程,一般在主线程中使用

参数:

  • thread:需要回收的子线程的ID
  • retval: 接收子线程退出时的返回值(即pthread_exit的void *retval参数), 而且是传出参数。

返回值:0 : 成功;非0 : 失败,返回的错误号

不使用传出参数的一个简单使用如下:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* callback(void* arg) {printf("子线程运行中...\n");sleep(2);
}int main()
{pthread_t pid;int ret = pthread_create(&pid, NULL, callback, NULL);if(ret != 0) {// 说明创建失败char * errstr = strerror(ret);printf("error: %s\n", errstr);}pthread_join(pid, NULL);printf("子线程已回收\n");return 0;
}

子线程执行2秒后,主进程才输出“子线程已回收”,说明pthread_join函数是阻塞的。

pthread_join函数比较难以理解的地方是他的第二个参数:void **retval,是void二级指针类型,这是因为:

首先这个参数是想接收pthread_exit所传出的void *retval, 这个参数本身是void *的一级指针类型,而pthread_join函数的void **retval在设计时是设计成一个传出参数的,以便把pthread_exit传出的void *retval带回主线程,所以要想把 void * 类型变量设计成传出参数,即是 void **。

示例程序如下:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>int value = 10;void* callback(void* arg) {printf("子线程运行中...\n");pthread_exit((void *)&value);
}int main()
{pthread_t pid;int ret = pthread_create(&pid, NULL, callback, NULL);if(ret != 0) {// 说明创建失败char * errstr = strerror(ret);printf("error: %s\n", errstr);}int *thread_retval;  // 给pthread_join调用,接收pthread_exit的传出参数pthread_join(pid, (void **)&thread_retval);printf("exit data : %d\n", *thread_retval);return 0;
}

运行结果如下:
在这里插入图片描述

2.5 线程分离:pthread_detach

#include <pthread.h>
int pthread_detach(pthread_t thread);

功能:使进程处于分离状态。被分离的线程在终止的时候,会自动释放资源返回给系统,避免产生僵尸线程。

线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。

参数:需要分离的线程的ID

返回值:成功:0,失败:返回错误号

注意:

  1. 线程不能多次分离,会产生不可预料的行为。
  2. 不能去连接(pthread_join)一个已经分离的线程,会报错:一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

2.6 线程取消:pthread_cancel

#include <pthread.h>
int pthread_cancel(pthread_t thread);

功能:取消线程(让线程终止)

【注意】:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write…
执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。
可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调用pthreestcancel函数自行设置一个取消点。

看下面这个代码示例,子线程无限循环:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* callback(void* arg) {while(1) {printf("子线程运行中...\n");sleep(1);}return NULL;
}int main()
{pthread_t pid;int ret = pthread_create(&pid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error: %s\n", errstr);}pthread_cancel(pid);ret = pthread_join(pid, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error: %s\n", errstr);}printf("线程已回收\n");return 0;
}

运行后成功输出”线程已回收“, 这是因为pthread_cancel终止了子线程的运行,故pthread_join得以执行。

但是如果将子进程中循环语句中的内容去掉:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>void* callback(void* arg) {while(1) {// printf("子线程运行中...\n");// sleep(1);}return NULL;
}int main()
{pthread_t pid;int ret = pthread_create(&pid, NULL, callback, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error: %s\n", errstr);}pthread_cancel(pid);ret = pthread_join(pid, NULL);if(ret != 0) {char * errstr = strerror(ret);printf("error: %s\n", errstr);}printf("线程已回收\n");return 0;
}

运行以后发现没有输出,主线程阻塞。这是因为子线程的while(1)死循环中没有任何语句,也就不会执行任何系统调用,也就不会到达任何一个“取消点”,所以子线程并没有被终止,主线程被阻塞在pthread_join处。而之前的代码循环语句中的printf会调用系统调用write,所以会到达“取消点”,pthread_join将已经结束的子线程回收。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384559.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux网络编程学习】预备知识(网络字节序、IP地址转换函数、sockaddr数据结构)

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 网络字节序 我们已经知道&#xff0c;内存中的多字节数据相对于内存地址有大端和小端之分。 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分&#xff0c;那么如何定义网络数…

【Linux网络编程学习】socket API(socket、bind、listen、accept、connect)及简单应用

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 什么是socket 所谓 socket&#xff08;套接字&#xff09;&#xff0c;就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。 一个套接字就是网络上进程通信的一端&#xff0c;提供了应用层进程利用网络协议交换…

【Linux网络编程学习】使用socket实现简单服务器——多进程多线程版本

此为牛客Linux C课程和黑马Linux系统编程笔记。 1. 多进程版 1.1 思路 大体思路与上一篇的单进程版服务器–客户端类似&#xff0c;都是遵循下图&#xff1a; 多进程版本有以下几点需要注意&#xff1a; 由于TCP是点对点连接&#xff0c;服务器主进程连接了一个客户端以后…

【Linux网络编程学习】I/O多路复用——select和poll

此为牛客Linux C课程和黑马Linux系统编程笔记。 0. I/O多路复用 所谓I/O就是对socket提供的内存缓冲区的写入和读出。 多路复用就是指程序能同时监听多个文件描述符。 之前的学习中写了多进程和多线程版的简单服务器模型&#xff0c;但是有个问题&#xff1a;每次新来一个客…

【Linux网络编程学习】阻塞、非阻塞、同步、异步以及五种I/O模型

文章目录1. 基本概念1.1 阻塞与非阻塞1.2 同步与异步1.3 为什么没有“异步阻塞”2. 五种IO模型2.1 阻塞 blocking2.2 非阻塞 non-blocking2.3. IO复用&#xff08;IO multiplexing&#xff09;2.4 信号驱动&#xff08;signal-driven&#xff09;2.5 异步&#xff08;asynchron…

STM32时钟树解析

本人之前其实也用STM32做过一些小东西&#xff0c;但因为时钟的初始化一般是直接在SystemInit时钟系统初始化函数里直接配置为72MHz&#xff0c;所以对于STM32的时钟框图并没有怎么理会&#xff0c;今天刚好有空就重新看了一下并写一篇博客记录一下吧&#xff0c;以免以后又忘了…

S3C2440时钟体系

S3C2440在默认情况下&#xff0c;整个系统全靠一个12MHz的外部晶振提供频率来工作运行的&#xff0c;也就是说CPU、内存、UART、ADC等所有需要用到时钟频率的硬件都工作在12MHz下&#xff0c;但是通过查阅芯片手册我们知道CPU时钟最高可为400MHZ&#xff0c;那么怎么设置时钟让…

关于MCU、CPU扩展SDRAM的一个小知识

像上图这种硬件电路图上的16个数据位和我们在初始化SDRAM的时候设置的16位数据位宽是指我们读写SDRAM的时候可以同时读写16个数据位&#xff0c;数据线越多肯定越快&#xff0c;但是数据线也不可能无限增加&#xff0c;我们在程序里是可以读写8位&#xff0c;16位&#xff0c;3…

S3C2440扩展SDRAM

本文主要目的是记录一下S3C2440扩展SDRAM的一些知识&#xff0c;方便以后查阅。 通过查阅手册我们知道&#xff0c;2440有8个可以用来扩展内存的BANK&#xff0c;其中第6和第7还可用来扩展SDRAM 下面我们来看一下2440扩展SDRAM需要设置哪些寄存器。 一、BWSCON寄存器 该寄存器…

汇编语言的相对跳转和绝对跳转以及反汇编代码解析

上图第一行的b1 main为相对跳转&#xff0c;即跳转到pcoffset,其中pc为当前pc值&#xff0c;offset可以理解为偏移地址&#xff0c;也就是根据当前所在地址加上偏移地址实现跳转&#xff0c;为相对跳转。 我们来看看它的反汇编代码 上图清除完bss区后使用b1指令跳转到30000668…

韦东山嵌入式第一期14课第004节_und异常模示程序示例_P笔记

本节课的第一个程序韦老师是想让大家见识一下未定义异常&#xff0c;而第二个程序是对第一个程序进行改进&#xff0c;防止在某些条件下执行不了&#xff0c;下面就来讲一下第2个程序改进了哪些地方并且有什么用。 程序在此路径中&#xff1a;源码文档图片\源码\源码_20180321…

关于NOR FLASH地址左右移的问题

问题引入&#xff1a;不知道你会不会有这样的疑问&#xff1a;为什么在发送解锁命令时&#xff0c;我们不用右移一位&#xff0c;而发送扇区地址时却要右移一位&#xff08;nor_cmd函数内部已经左移一位&#xff09;&#xff0c;这里先补充说明一下说明是cpu角度和nor角度&…

在linux下利用ls命令进行模糊查找

如上图&#xff0c;我们当前路径下有三个文件&#xff0c;分别为helloworld.c以及helloworld和1.c&#xff0c;直接输入命令ls则显示所有文件&#xff0c;我们可以利用ls 加*的方向进行模糊查找。 输入ls 目录名 形式的命令行&#xff0c;则是对该目录名下的文件全部进行显示&a…

Linux下没有包含头文件(不知是哪个)导致编译无法通过的解决心得

最近写程序的时候编译出错了&#xff0c;提示信息为&#xff1a;invalid use of undefined type fb_var_screeninfo。显示根据英文知道是没有定义 fb_var_screeninfo这个类型&#xff0c;明显是缺少了某个头文件&#xff0c;但是缺少哪个头文件以及有什么又快又好的解决方法呢&…

Linux编译程序时加-I指定头文件位置

Linux下编译出现以下错误&#xff0c;错误的原因是在/usr/local/arm/arm-2009q3/bin/../arm-none-linux-gnueabi/libc/usr/include/freetype/config/下找不到ftheader.h&#xff0c;而我到该目录下看&#xff0c;发现路径是这样的rootubuntu:/usr/local/arm/arm-2009q3/arm-non…

关于源文件用不同的编码方式编写,会导致执行结果不一样的现象及解决方法

如果我们编写以下程序&#xff0c;并分别另存为ANSI和UTF-8两种不同的编码方式保存&#xff0c;放到Linux下编译并运行如下图&#xff0c;两端相同的程序以不同的编码方式保存编译后的运行结果不一样&#xff0c;./ansi采用ANSI编码方式&#xff0c;会自动采用GBK方式来保存中文…

arm-linux-gcc静态编译和动态编译的区别

很多教程会提到加上-static是静态编译&#xff0c;但对于新手来说没有用例子来说明可能不太好理解&#xff0c;今天我就介绍一下关于这方面知识的一个例子&#xff1a; 最近在做一个关于freetype字体的东西&#xff0c;需要依赖freetype官方提供的库&#xff0c;我已经把电脑这…

从0到1写RT-Thread内核——线程定义及切换的实现

从0写RT-Thread内核之线程定义及切换的实现具体可以分为以下六步来实现 一&#xff1a;分别定义线程栈、线程函数、线程控制块&#xff1b; ALIGN(RT_ALIGN_SIZE)//设置4字节对齐 /* 定义线程栈 */ rt_uint8_t rt_flag1_thread_stack[512]; rt_uint8_t rt_flag2_thread_stack…

从0到1写RT-Thread内核——临界段的保护

临界段就是一段在执行的时候不能被中断的代码段&#xff0c;在RT-Thread里&#xff0c;临界段最常出现的就是对全局变量的操作&#xff08;类似Linux下的锁&#xff09;。RT-Thread对临界段的保护是直接把中断全部关了&#xff0c;NMI FAULT和硬FAULT除外。下图是3个关于中断屏…

从0到1写RT-Thread内核——空闲线程与阻塞延时的实现

在之前写的另外一篇文章——<从0到1写RT-Thread内核——线程定义及切换的实现>中线程体内的延时使用的是软件延时&#xff0c;即还是让CPU空等来达到延时的效果。RTOS中的延时叫阻塞延时&#xff0c;即线程需要延时的时候&#xff0c;线程会放弃CPU的使用权&#xff0c;C…