【为项目做准备】Linux操作系统day1

这系列文章是以专栏:趣谈Linux操作系统,Linux实战技能100讲以及网络编程实战为基础的,因为笔者基础薄弱,这次学习是为了秋招大项目sylar以及计算机基础做准备。

入门:

安装

dpkg安装

win下载exe,对于Linux来讲,也是类似的方法,可以下载rpm或者deb。这个就是Linux下面的安装包。为什么有两种呢?
因为Linux现在常用的有两大体系,一个是CentOS体系,一个是Ubuntu体系,前者使用rpm,后者使用deb。
以下只记录Ubuntu。
以下载jdk为例:

  1. 下载jdk.deb文件。
    这一步在ubantu系统的浏览器中进行,访问openJDK的官网,选择合适的.deb文件进行下载。通常会默认保存到~/Downloads文件夹。
  2. 打开终端
    在ubantu中,打开终端除了去应用里找还可以通过按 Ctrl + Alt + T 实现。
  3. 使用终端按照JDK
    通过打开的终端,还需要使用cd命令切换到包含已下载.deb文件的目录。通常是~/Downloads。
    然后使用sudo dpkg -i 文件名.deb命令在ubantu上安装JDK。这个步骤设计权限提升,所以需要使用sudo。
  4. 验证JDK的安装
    还是在同一个终端内,可以通过输入java -version来检查JDK是否正确安装。
查看安装

可以用dpkg -l查看安装的软件列表,-q就是query,a就是all,-l的意思就是list。

这个列表会很长,可以用grep搜索工具来找到特定的软件。

dpkg -l | grep jdk这个命令是将列出来的所有软件形成一个输出。| 是管道,用于连接两个程序,前面dpkg -l的输出就放进管道里面,然后作为grep的输入,grep将在里面进行搜索带关键词jdk的行,并且输出出来。

grep支持正则表达式,因此搜索的时候很灵活,再加上管道,这是一个很常用的模式。

如果你不知道关键词,可以使用dpkg -l | more和dkpg -l | less这两个命令,它们可以将很长的结果分页展示出来。这样你就可以一个个来找了。

more是分页后只能往后翻页,翻到最后一页自动结束返回命令行,less是往前往后都能翻页,需要输入q返回命令行,q就是quit。

删除

如果需要删除,可以用dpkg -r,-r就是remove。后面要接安装包,具体安装包是什么名字,用查看安装的方式看。

apt-get安装

相当于ubuntu的软件管家

可以根据关键词搜索,例如搜索jdk

yum search jdkapt-cache search jdk,可以搜索出很多很多可以安装的jdk版本。

如果数目太多,你可以通过管道grep、more、less来进行过滤。

选中一个之后,我们就可以进行安装了。你可以用yum install java-11-openjdk.x86\_64apt-get install openjdk-9-jdk来进行安装。

卸载

可以使用yum erase java-11-openjdk.x86\_64apt-get purge openjdk-9-jdk

Linux允许我们配置从哪里下载这些软件的,地点就在配置文件里面。

对于Ubuntu来讲,配置文件在/etc/apt/sources.list里。

其实无论是先下载再安装,还是通过软件管家进行安装,都是下载一些文件,然后将这些文件放在某个路径下,然后在相应的配置文件中配置一下。

例如,在Windows里面,最终会变成C:\Program Files下面的一个文件夹以及注册表里面的一些配置。

对应Linux里面会放的更散一点。

例如,主执行文件会放在/usr/bin或者/usr/sbin下面,其他的库文件会放在/var下面,配置文件会放在/etc下面。

解压缩下载

还有一种简单粗暴的方式就是将安装好的路径直接下载下来,然后解压缩成为一个整的路径。
首先需要将压缩包下载下来,win中一般是下载zip包,在Linux环境下一般下载的是.tar.gz的包,下载方式是在链接前面加上wget。

下载之后还需要解压缩,将压缩文件解压,win有winzip这种软件一键解压,Linux环境下可以使用tar程序,比如通过tar xvzf jdk-XXX_linux-x64_bin.tar.gz就可以解压缩了。

通过tar解压缩之后,需要配置环境变量,可以通过export命令来配置,相当于win中配置JAVA_HOME和PATH
export JAVA_HOME=/ROOT/JDK-XXX_LINUX-X64
export PATH=$JAVA_HOME/bin:$PATH
export命令仅在当前命令行的会话中管用,一旦退出重新登录进来,就不管用了。

如何让设置永久有效呢?在当前用户的默认工作目录中,如/root或者/home/cliu8下面,有一个.bashrc文件,这个文件是以点开头的,这个文件默认看不到,需要ls -la才能看到,a就是all。每次登录的时候,这个文件都会运行,因而把它放在这里。这样登录进来就会自动执行。当然也可以通过source .bashrc手动执行。

要编辑.bashrc文件,可以使用文本编辑器vim。如果默认没有安装,可以通过yum install vim及apt-get install vim进行安装。

通过vim .bashrc,将export的两行加入后,输入:wq,写入并且退出,这样就编辑好了。

运行

通过./filename

win的话点击exe就行
Linux不是根据后缀名来运行的。它的执行条件是这样的:只要文件有执行权限,都能到文件所在的目录下,通过./filename运行这个程序,前提是生成了可执行文件,.o或.c什么的都不对。

这是 Linux执行程序最常用的一种方式,通过shell在交互命令行里面运行

这种方式比较适合运行一些简单的命令,例如通过date获取当前时间。这种模式的缺点是,一旦当前的交互命令行退出,程序就停止运行了。

另外,假如没有生成可执行文件,仅仅只是编译了一下,那么gcc编译完是会默认输出一个文件是a.out的可执行文件,执行这个文件也可以。
就./a.out即可。

后台通过nohup运行

Linux运行程序的第二种方式,后台运行。
使用nohup命令。这个命令的意思是no hang up(不挂起),也就是说,当前交互命令行退出的时候,程序还要在。
最后加一个&,就表示后台运行,不会霸占命令行。

另外一个要处理的就是输出,原来什么都打印在交互命令行里,现在在后台运行了,输出到哪里呢?
输出到文件是最好的。
最终命令的一般形式为nohup command >out.file 2>&1 &

其中1表示文件描述符1,表示标准输出,2表示文件描述符2,表示标准错误输出,2>&1表示标准输出和错误输出合并了。合并到out.file里。

这个后台运行的进程如何关闭呢?假设启动的程序包含某个关键字,那么就可以使用下面的命令。
ps -ef |grep 关键字 |awk '{print $2}' |xargs kill -9
其中ps -ef可以单独执行,列出所有正在运行的程序,grep可以通过关键字找到咱们刚刚启动的程序。
awk工具可以很灵活地对文本进行处理,这里的awk '{print $2}'是指第二列的内容,是运行的程序ID。

我们可以通过xargs传递给kill -9,也就是发给这个运行的程序一个信号,让它关闭。

如果你已经知道运行的程序ID,可以直接使用kill关闭运行的程序。

以服务的方式运行

运行程序的第三种方式,以服务的方式运行。

例如常用的数据库MySQL,就可以使用这种方式运行。

例如在Ubuntu中,我们可以通过apt-get install mysql-server的方式安装MySQL,然后通过命令systemctl start mysql启动MySQL,通过systemctl enable mysql设置开机启动。

之所以成为服务并且能够开机启动,是因为在/lib/systemd/system目录下会创建一个XXX.service的配置文件,里面定义了如何启动、如何关闭。

学几个系统调用

进程管理

  • 启动的时候先创建一个所有用户进程的"祖宗进程"。
  • 父进程用fork调用子进程,创建的时候会将父进程的数据结构以及程序代码都拷贝一份给子进程。
  • 对于fork系统调用的返回值,如果当前进程是子进程,就返回0;如果当前进程是父进程,就返回子进程的进程号。
  • 然后通过if-else语句判断,如果是父进程,还接着做原来应该做的事情;如果是子进程,需要请求另一个系统调用execve来执行另一个程序。
  • 父进程要关心子进程的运行情况,有个系统调用waitpid,父进程可以调用它,将子进程的进程号作为参数传给它,这样父进程就知道子进程运行完了没有,成功与否。

内存管理

  • 每个进程都有自己独立的进程内存空间。
  • 项目执行计划书,相当于放进进程内存空间中的程序代码,称为代码段。
  • 对于进程的内存空间来讲,放进程运行中产生数据的这部分,我们称为数据段;其中局部变量的部分,在当前函数执行的时候起作用,当进入另一个函数时,这个变量就释放了;也有动态分配的,会较长时间保存,指明才销毁的,这部分称为
  • 物理空间有限,只有真的写入数据的时候,发现没有对应物理内存,才会触发一个中断,现分配物理内存。

介绍两个在堆里面分配内存的系统调用,brk和mmap。

  • 当分配的内存数量比较小的时候,使用brk,会和原来的堆的数据连在一起。
  • 当分配的内存数量比较大的时候,使用mmap,会重新划分一块区域。

文件管理

进程程序有一些是需要长时间保存的,就需要放在文件系统里。对于文件的操作,下面系统调用是重要的:

  • 对于已有的文件,可以使用open打开这个文件,close关闭。
  • 对于没有的文件,可以使用creat创建文件。
  • 打开文件以后,可以使用lseek跳到文件的某个位置。
  • 可以对文件的内容进行读写,读的系统调用是read,写的系统调用是write。

Linux有一个特点,就是一切皆文件:

  • 启动一个进程,需要一个程序文件,这是一个二进制文件。
  • 启动的时候,要加载一些配置文件,例如yml、properties等,这是文本文件;启动之后会打印一些日志,如果写到硬盘上,也是文本文件
  • 但是如果我想把日志打印到交互控制台上,在命令行上唰唰地打印出来,这其实也是一个文件,是标准输出stdout文件
  • 这个进程的输出可以作为另一个进程的输入,这种方式称为 管道,管道也是一个文件。
  • 进程可以通过网络和其他进程进行通信,建立的 Socket,也是一个文件。
  • 进程需要访问外部设备, 设备 也是一个文件。
  • 文件都被存储在文件夹里面,其实 文件夹 也是一个文件。
  • 进程运行起来,要想看到进程运行的情况,会在/proc下面有对应的 进程号,还是一系列文件。

每个文件,Linux都会分配一个文件描述符,这是一个整数。
有了这个文件描述符,我们就可以使用系统调用,查看或者干预进程运行的方方面面。所以说,文件操作是贯穿始终的,这是一切皆文件的优势。

项目异常处理与信号处理

项目遇到异常情况,就会需要发送一个信号。
什么样的异常信号发送给谁呢?

  • 在执行一个程序的时候,在键盘输入“CTRL+C”,这就是中断的信号,操作系统发送一个"SIGINT"(signal interrupt)信号给当前正在执行的程序,让他立刻停止。
  • 如果非法访问内存,操作系统发送一个"SIGSEGV"信号,这个信号的接受者是试图非法访问内存的进程
  • 当使用kill命令或函数时,操作系统会向指定的进程发送一个信号。信号的接收者是被指定的目标程序(通过进程ID或PID标识)。

进程间通信

进程之间的沟通方式有很多,首先是发个消息,不需要很长的数据,这种方式称为消息队列,通过msgget创建一个新的队列,msgsnd将消息发送到消息队列,而消息接收方可以使用msgrcv从队列中取消息。
需要交互的信息比较大的时候,可以使用共享内存的方式,就像两个项目组共享某一个会议室,这样数据也不需要拷来拷去。这时候,通过shmget创建一个共享内存块,通过shmat将共享内存映射到自己的内存空间,然后就可以读写了。
共享的这一块必然会存在资源竞争的问题,这就需要信号量机制的参与与管控。

网络通信

不同的Linux服务器需要交流必须遵循相同的网络协议,即TCP/IP网络协议栈。Linux内核中有对于网络协议栈的实现。

如何使用呢?用socket,可以通过socket系统调用建立一个socket,socket也是一个文件,也有一个文件描述符,也可以通过读写函数进行通信。
网络服务是通过套接字socket来提供服务的,可以想象成弄一根网线,一头插在客户端,一头插在服务端,然后进行通信,在通信之前,双方都要建立一个socket。

总结
在这里插入图片描述

实操记录:

创建进程

在Linux上写程序和编译程序,也想要一套开发套件,基于centOS 7的操作系统是执行命令yum -y groupinstall "Development Tools"
但是我只有ubantu,系统不同所以用的命令是:

//更新软件包列表,确保安装时使用最新的软件包信息
sudo apt update
//安装基本的开发工具集合,包括gcc,g++,make,以及其他开发所需要的工具和库
sudo apt install build-essential

在执行sudo apt update的时候遇到报错

ubuntu@ubuntu-virtual-machine:~$ sudo apt update
命中:1 http://security.ubuntu.com/ubuntu focal-security InRelease              
错误:2 http://cn.archive.ubuntu.com/ubuntu focal InRelease                     
  无法发起与 cn.archive.ubuntu.com:80 (2403:2c80:5::6) 的连接 - connect (101: 网络不可达) 无法连接上 cn.archive.ubuntu.com:80 (45.125.0.6)。 - connect (111: 拒绝连接)
错误:3 http://cn.archive.ubuntu.com/ubuntu focal-updates InRelease
  无法发起与 cn.archive.ubuntu.com:80 (2403:2c80:5::6) 的连接 - connect (101: 网络不可达)
**剩余错误省略**

原因在于ubantu无法连接到中国的镜像服务器,为了解决这个问题,可以更换ubantu软件源到一个可用的服务器。比如官方的ubantu服务器或者其他镜像服务器。
解决:
第一步,先备份
这一步主要是害怕一次更换不成功

sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

第二步,编辑源列表文件
这一步使用nano或者vim编辑都可以
nano是服务器自带的,vim需要下载,所以在这一步使用不了vim,因为我网络有问题啥都无法下载

sudo nano /etc/apt/sources.list

第三步,替换为清华大学镜像源
首先将sources.list文件内全部清空,Ctrl+K即可逐行删除
在将以下代码粘贴进去

# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse

nano语法中,按 Ctrl+O 保存文件,然后按 Enter 确认。按 Ctrl+X 退出编辑器。

之后就可以顺利执行sudo apt update了。

执行完毕后,把vim下载了,在把build-essential下载了。

写一个函数封装创建进程的逻辑,命名为process.c
vim环境,代码为:

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

extern int create_process (char* program, char** arg_list);
int create_process (char* program, char** arg_list)
    {
        pid_t child_pid;
        child_pid = fork ();
        if (child_pid != 0)
            return child_pid;
        else {
            execvp (program, arg_list);
            abort ();
        }
}

上面代码用到了fork,通过if-else根据返回值的不同,父子进程分道扬镳,在子进程里,通过execvp运行一个新程序。

接下来创建第二个文件createprocess.c,调用上面的函数,并运行一些简单的命令。

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

extern int create_process (char* program, char** arg_list);

int main ()
{
    char* arg_list[] = {
        "ls",
        "-l",
        "/etc/yum.repos.d/",
        NULL
    };
    create_process ("ls", arg_list);
    return 0;
}

以上两个文件都是.c,其实就是文本文件,为了让cpu执行,还需要先将其编译一下,编译成cpu看得懂的二进制文件
在Linux下,二进制也有严格的格式,这个格式就是ELF。这个格式可以根据编译的结果不同,分为不同的格式。
接下来编译这两个文件:

gcc -c -fPIC process.c
gcc -c -fPIC createprocess.c

编译成.o文件,就是ELF的第一种类型,可重定位文件。

#--------------------------------------------------------------
ok,现在把从源代码编译生成对象文件(.o),然后将对象文件打包成静态或动态链接库,最后将这些库链接到可执行文件中的过程整体走一遍。

  1. 编译过程:
gcc -c -fPIC process.c
gcc -c -fPIC createprocess.c

此处需要解释!!

2. gcc的编译指令不一定需要-fPIC,-fPIC是一种特定的编译选项,他主要用于生成位置无关代码(position-independent code,PIC)。
3. 位置无关代码的特点是,他在程序的地址空间中可以被加载到任何位置而无需修改。他的地址引用是相对的而不是绝对的,这对于共享库(例如动态链接库.so文件)特别重要。
4. 指令解释:-c,只编译,不进行链接
5. 使用 -fPIC 的主要目的是为了生成共享库(动态库),使得编译生成的位置无关代码可以在内存中的任何位置正确执行。
6. 在生成静态库或普通可执行文件时,不使用 -fPIC 也是常见的,因为这些情况不需要位置无关性。

使用 gcc 编译器,将源文件 (.c 文件) 编译成对象文件 (.o 文件)。这些对象文件是 可重定位文件,即它们可以在程序的任何位置使用,但在被使用前需要被"重定位"。

  1. 静态链接库(.a文件):
//生成静态链接库
ar cr libstaticprocess.a process.o
//静态链接库生成可执行文件
gcc -o staticcreateprocess createprocess.o -L. -lstaticprocess

此处需要解释!!

1. ar是归档命令,c是创建,r是替换,ar cr是将1文件归档成2文件,如果2文件不存在则创建,存在则替换。所以第一条指令是指,将process.o文件归档到静态库libstaticprocess.a中。
2. 静态链接库的命名规则解释:lib<name>.a;所以libstaticprocess.a是一个自命名的静态库,staticprocess是库实际名称
3. -l指定库名,使用-l<name>指定要链接的库,比如-lstaticprocess会自动查找名为libstaticprocess.a的文件
4. -L指定gcc在当前目录下查找库文件
5. 第二条命令总结就是:编译器首先会读取目标文件createprocess.o,再用-L查找库,找到库之后会提取staticprocess.a库中包含的对象文件process.o,编译器将目标文件和库进行链接,最后生成一个名为staticcreateprocess的可执行文件。

通过 ar 工具,将多个 .o 文件打包成一个静态链接库。静态链接库在链接到可执行文件时,其内容会被复制到可执行文件中,因此每个程序都会有自己的一份库文件代码。

  1. 动态链接库(.so文件):
//创建动态链接库
gcc -shared -fPIC -o libdynamicprocess.so process.o
//链接动态库生成可执行文件
gcc -o dynamiccreateprocess createprocess.o -L. -ldynamicprocess

此处需要解释!!

1. -shared 指定编译器生成一个共享对象文件,即动态链接库(.so 文件)
2. -fPIC,生成位置无关代码
3. -o libdynamiccreateprocess.so指定输出的文件名
4. process.o是要链接的目标文件
其他的跟静态差不多

通过 gcc -shared,将对象文件组合成一个动态链接库。动态链接库不会在编译时被复制到可执行文件中,而是在程序运行时动态加载到内存中,多个程序可以共享一个动态链接库的代码段。

  1. PLT(过程链接表)和GOT(全局偏移量表):

当可执行文件调用动态链接库中的函数时,它首先通过 PLT 调用代理代码来查找该函数的地址。代理代码会查询 GOT,如果 GOT 中的地址未被初始化,它会调用动态链接器 ld-linux.so 来确定函数的实际地址并将其填入 GOT,之后可以直接使用该地址。

#-----------------------------------------------------------------
上面是一个总体生成的过程,那么细节呢?过一遍吧。
ELF文件加载与执行过程:

  • 在Linux内核中,有专门的结构体
    linux_binfmt用于定义加载不同二进制文件的方式,对于ELF文件,有个对应的实现,定义了load_elf_binary用于加载elf格式的可执行文件。
  • elf文件的加载函数load_elf_binary最终由exec系统调用触发,exec是一组函数(execv,execvp和execve等),它们负责将指定的程序加载到当前进程的地址空间并开始执行。
  • 所有的进程都是通过父进程fork创建的,根进程是系统启动时的init(PID1);运行中的进程分为用户态进程和内核态进程,用户态进程的祖先是init进程(PID1),而内核态进程的祖先是内核线程(PID2).

编译与链接过程(文件系统层面):
源代码 -> .o 文件 -> .so 动态库或可执行文件。

进程加载与执行过程(用户态与内核态):
用户态进程 A 调用 fork 创建进程 B,进程 B 执行 exec 系列系统调用。
内核中通过 load_elf_binary 将可执行文件加载到内存中,进程 B 开始执行。

有多种工具可以用于分析和查看ELF文件:

  • readelf:分析 ELF 文件信息。
  • objdump:显示二进制文件信息。
  • hexdump:查看文件的十六进制编码。
  • nm:显示指定文件中符号的信息。

具体使用:
使用readelf工具分析ELF

readelf -h my_program.o #查看ELF文件头信息
readelf -S my_library.so #查看段表信息
readelf -l my_executable #查看程序头信息(对于可执行文件和共享库很重要)
readelf -s my_executable #查看符号表

使用objdump工具显示二进制文件信息

objdump -d my_program.o #反汇编查看可执行代码
objdump -h my_library.so #显示段信息
objdump -t my_executable #显示符号表
objdump -T my_library.so #显示动态符号表(对共享库文件有用)

使用hexdump工具查看十六进制编码

hexdump -C my_program.o #以16进制格式显示文件内容
hexdump -n 128 -s 0x20 -C my_library.so #指定长度和偏移量进行查看

使用nm工具显示符号信息

nm my_program.o #列出所有符号
nm -g my_executable #仅显示已定义的符号
nm -D my_library.so #显示动态符号(用于共享库)

具体实践,假设咱有三个文件,my_program.o目标文件
my_library.so共享库文件
my_executable可执行文件

现在要对以上文件进行分析,可以依次运行命令:

readelf -h my_program.o
objdump -d my_executable
hexdump -C my_library.so
nm -D my_library.so

通过这些命令,能够查看 ELF 文件的详细结构、符号信息和二进制内容,深入理解文件的内部工作原理。

创建线程

以进程相当于一个项目,而线程就是为了完成项目需求,而建立的一个个开发任务。每一个进程都默认有一个线程。Linux是多线程的。

创建一个多线程下载视频的模拟:
文件名download.c

#include <pthread.h>// 引入 POSIX 线程库
#include <stdio.h>// 引入标准输入输出库
#include <stdlib.h>// 引入标准库(例如用于 `exit` 函数)
#include <unistd.h>// 引入 Unix 标准库(例如用于 `sleep` 函数)
#define NUM_OF_TASKS 5 // 定义任务的数量为 5

//这是每个线程将执行的函数。它接收一个参数 filename,这个参数是一个指向字符串的指针,表示文件名。
void *downloadfile(void *filename)
{
   printf("I am downloading the file %s!\n", (char *)filename); // 输出当前线程正在下载的文件。
   sleep(10); // 模拟下载需要的时间,线程会暂停 10 秒。
   long downloadtime = rand()%100; // 生成一个 0 到 99 之间的随机数,模拟下载时间(以分钟为单位)。
   printf("I finish downloading the file within %ld minutes!\n", downloadtime);
   pthread_exit((void *)downloadtime); // 线程结束时调用,返回下载时间(以 long 类型返回)。
}

int main(int argc, char *argv[])
{
   char files[NUM_OF_TASKS][20]={"file1.avi","file2.rmvb","file3.mp4","file4.wmv","file5.flv"}; // 一个二维数组,包含 5 个文件名。
   pthread_t threads[NUM_OF_TASKS]; // 一个 pthread_t 类型的数组,用于存储线程 ID。
   int rc; // 存储线程创建函数的返回值
   int t; // 循环变量
   int downloadtime; // 用于接收线程的返回值

   pthread_attr_t thread_attr;
   pthread_attr_init(&thread_attr);
   pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_JOINABLE); // 设置线程为可连接状态(PTHREAD_CREATE_JOINABLE),表示主线程可以等待子线程完成。
   
	//使用 pthread_create 创建线程,传入线程 ID、线程属性、线程函数和线程参数。如果线程创建失败,打印错误信息并退出。
   for(t=0;t<NUM_OF_TASKS;t++){
     printf("creating thread %d, please help me to download %s\n", t, files[t]);
     rc = pthread_create(&threads[t], &thread_attr, downloadfile, (void *)files[t]);
     if (rc){
       printf("ERROR; return code from pthread_create() is %d\n", rc);
       exit(-1);
     }
   }

   pthread_attr_destroy(&thread_attr); // 销毁线程属性对象,释放资源

   for(t=0;t<NUM_OF_TASKS;t++){
     pthread_join(threads[t],(void**)&downloadtime); // 使用 pthread_join 等待每个线程结束,并获取线程返回的 downloadtime。
     printf("Thread %d downloads the file %s in %d minutes.\n",t,files[t],downloadtime);
   }

   pthread_exit(NULL);
}

线程的数据分类与管理
在多线程编程中,不同线程访问和操作的数据都可以分为三类:线程栈上的本地数据,共享的全局数据和线程私有数据。

  1. 线程栈上的本地数据
  • 每个线程都有自己的栈空间,用于存放函数调用的局部变量,线程栈的大小可以通过ulimit -a命令查看,默认大小是8M,可以通过ulimit -s进行调整。
  • 线程栈的大小也可以通过以下函数进行设置:int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
  1. 共享的全局数据
  • 在一个进程内,所有线程共享全局数据,多个线程同时修改一个全局变量时,会导致错误,需要锁机制来保护这些共享数据的安全访问。
  1. 线程私有数据(TSD)
  • 在同一个进程内,所有线程可以访问同一个键(key),但每个线程可以为该键设置不同的值。这样就实现了线程级别的“全局变量”,使得各个线程有自己的“隐私数据”。
  • 创建TSD函数:int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));创建一个键 key 和对应的析构函数 destructor,在线程退出时自动释放该键的值。
  • 设置TSD的值:int pthread_setspecific(pthread_key_t key, const void *value);为线程设置 key 对应的值 value。
  • 获取TSD的值:void *pthread_getspecific(pthread_key_t key);,获取当前线程中key对应的值。

共享数据保护与多线程编程:Mutex和条件变量
在多线程编程中,为了避免多个线程同时访问共享数据导致的数据不一致问题,我们使用了 Mutex(互斥锁) 和 条件变量 来控制线程的执行顺序和访问共享资源的方式。

  1. Mutex互斥锁
    Mutex 用于确保在任何时间只有一个线程能够访问共享数据。

此处急需给些POSIX内置函数!!!不然一点也看不懂!!!!
下面介绍POSIX线程库内置函数

  1. pthread_create

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

  • 作用:创建一个新的线程
  • 参数:thread指向线程标识符的指针,线程创建成功后会在这个变量中返回新的线程ID;attr指向线程属性对象的指针,如果是NULL则为默认属性;start_routine指向线程执行的函数的指针;arg传递给start_routine函数的参数
  • 返回值:成功返回0,失败返回错误代码
  1. pthread_exit

void pthread_exit(void *retval);

  • 作用:终止调用线程,并且可以返回一个指向某个对象的指针(retval)作为线程的退出状态。
  • 参数:retval线程返回的指针
  1. pthread_mutex_init

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

  • 作用:初始化互斥锁mutex
  • 参数:mutex指向需要初始化的互斥锁对象;attr指向互斥锁属性对象的指针,如果为NULL则使用默认属性。
  • 返回值:成功返回0,失败返回错误代码
  1. pthread_mutex_lock

int pthread_mutex_lock(pthread_mutex_t *mutex);

  • 作用:锁定互斥锁mutex,如果锁已经被另一个线程占用,则调用线程会阻塞,直到互斥锁被解锁
  • 参数:mutex指向需要锁定的互斥锁对象
  • 返回值:成功返回0,失败返回错误代码
  1. pthread_mutex_unlock

int pthread_mutex_unlock(pthread_mutex_t *mutex);

  • 作用:解锁互斥锁mutex
  • 参数:mutex指向需要解锁的互斥锁对象
  • 返回值:成功返回0,失败返回错误代码
  1. pthread_cond_init

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

  • 作用:初始化条件变量cond
  • 参数:cond指向需要初始化的条件变量对象;attr指向条件变量属性对象的指针。如果为 NULL,则使用默认属性。
  • 返回值:成功返回 0,失败返回错误代码。
  1. pthread_cond_wait

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

  • 作用:等待条件变量 cond,进入等待状态直到被唤醒。
  • 参数:cond指向条件变量的指针;mutex指向与条件变量相关联的互斥锁对象的指针。调用 pthread_cond_wait 时会解锁 mutex,并在被唤醒时重新锁定它。
  • 返回值:成功返回0,失败返回错误代码
  1. pthread_cond_broadcast

int pthread_cond_broadcast(pthread_cond_t *cond);

  • 作用:唤醒所有等待在条件变量cond上的线程。
  • 参数:cond指向条件变量的指针。
  • 返回值:成功返回 0,失败返回错误代码。
  1. pthread_mutex_destroy和pthread_cond_destroy

int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_cond_destroy(pthread_cond_t *cond);

  • 作用:销毁mutex和条件变量cond
  • 返回值:成功返回 0,失败返回错误代码。

举例:转账红包
有两个员工Tom和Jerry,公司食堂的饭卡里面各自有100元,并行启动5个线程,都是Jerry转10元给Tom,主线程不断打印Tom和Jerry的资金之和。按说,这样的话,总和应该永远是200元。

来看代码:

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

#define NUM_OF_TASKS 5

int money_of_tom = 100;// Tom 的初始资金为 100
int money_of_jerry = 100;// Jerry 的初始资金为 100
// 定义一个互斥锁用于保护资金变量的并发访问
pthread_mutex_t g_money_lock;

void *transfer(void *notused)
{
  pthread_t tid = pthread_self();// 获取当前线程的ID
  printf("Thread %u is transfering money!\n", (unsigned int)tid);
  
  pthread_mutex_lock(&g_money_lock);// 加锁,防止其他线程同时修改资金变量
  
  sleep(rand()%10);// 随机等待,模拟资金转账过程
  money_of_tom+=10;// Tom 收到10元
  sleep(rand()%10);// 随机等待,模拟资金转账过程
  money_of_jerry-=10;// Jerry 转出10元
  
  pthread_mutex_unlock(&g_money_lock);// 解锁,允许其他线程访问资金变量
  printf("Thread %u finish transfering money!\n", (unsigned int)tid);
  pthread_exit((void *)0);// 线程正常退出
}

int main(int argc, char *argv[])
{
  pthread_t threads[NUM_OF_TASKS];// 线程ID数组
  int rc;
  int t;
  
  pthread_mutex_init(&g_money_lock, NULL);// 初始化互斥锁

  for(t=0;t<NUM_OF_TASKS;t++){
    rc = pthread_create(&threads[t], NULL, transfer, NULL);// 创建新线程,执行transfer函数
    if (rc){
      printf("ERROR; return code from pthread_create() is %d\n", rc);
      exit(-1);// 创建线程失败时退出
    }
  }

  for(t=0;t<100;t++){
    
    pthread_mutex_lock(&g_money_lock);// 加锁,保护资金总和计算
    printf("money_of_tom + money_of_jerry = %d\n", money_of_tom + money_of_jerry); 
    pthread_mutex_unlock(&g_money_lock); // 解锁
  }
  
  pthread_mutex_destroy(&g_money_lock);// 销毁互斥锁
  pthread_exit(NULL);// 主线程退出
}

假如将注释的地方都去掉,也就是删除有关mutex的行。
现在编译运行一下,会发现money_of_tom + money_of_jerry会逐渐超过200…
所以中间有很多状态不确定,两个人账户之和超过200的情况就是Tom转入了,Jerry还没转出!
加上mutex之后就正常了。

  1. 条件变量

条件变量用于线程间的同步和通知,通常与 Mutex 配合使用。

  • 等待:线程调用 pthread_cond_wait() 等待条件变量,期间会释放 Mutex 锁并进入等待状态。
  • 通知:当条件满足时,调用 pthread_cond_signal()pthread_cond_broadcast() 唤醒等待线程。
  • 初始化和销毁:分别使用 pthread_cond_init()pthread_cond_destroy() 初始化和销毁条件变量。

举例:员工抢任务
你是这个老板,招聘了三个员工,但是你不是有了活才去招聘员工,而是先把员工招来,没有活的时候员工需要在那里等着,一旦有了活,你要去通知他们,他们要去抢活干(为啥要抢活?因为有绩效呀!),干完了再等待,你再有活,再通知他们。

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

#define NUM_OF_TASKS 3 // 3个线程
#define MAX_TASK_QUEUE 11 // 任务队列的最大长度是11

char tasklist[MAX_TASK_QUEUE]="ABCDEFGHIJ"; // 任务队列,包含了10个任务(从'A'到'J')。
// 分别表示任务队列的头指针和尾指针,初始值为0。
int head = 0;
int tail = 0;
// 用于控制线程的终止,当它为1时,所有线程都会退出。
int quit = 0;

pthread_mutex_t g_task_lock;//全局互斥锁,用于保护共享资源(如任务队列)。
pthread_cond_t g_task_cv;//全局条件变量,用于线程同步。

//该函数定义了每个线程的工作内容,即从任务队列中取任务并执行。
void *coder(void *notused)
{
  pthread_t tid = pthread_self();//获取当前线程的ID

  while(!quit){

    pthread_mutex_lock(&g_task_lock);//获取锁,确保安全访问任务队列
    while(tail == head){//如果没有任务(tail == head 表示任务已被取完)
      if(quit){//检查是否退出
        pthread_mutex_unlock(&g_task_lock);//解锁
        pthread_exit((void *)0);//退出线程
      }
      printf("No task now! Thread %u is waiting!\n", (unsigned int)tid);
      pthread_cond_wait(&g_task_cv, &g_task_lock);//线程等待条件变量的通知,同时释放锁
      printf("Have task now! Thread %u is grabing the task !\n", (unsigned int)tid);
    }
    char task = tasklist[head++];// 取任务,并将 head 指针向前移动
    pthread_mutex_unlock(&g_task_lock);// 释放锁
    printf("Thread %u has a task %c now!\n", (unsigned int)tid, task);
    sleep(5);// 模拟任务执行
    printf("Thread %u finish the task %c!\n", (unsigned int)tid, task);
  }

  pthread_exit((void *)0);// 退出线程
}

int main(int argc, char *argv[])
{
  pthread_t threads[NUM_OF_TASKS];
  int rc;
  int t;

  pthread_mutex_init(&g_task_lock, NULL);// 初始化互斥锁
  pthread_cond_init(&g_task_cv, NULL);// 初始化条件变量
  // 创建 NUM_OF_TASKS 个线程
  for(t=0;t<NUM_OF_TASKS;t++){
    rc = pthread_create(&threads[t], NULL, coder, NULL);
    if (rc){
      printf("ERROR; return code from pthread_create() is %d\n", rc);
      exit(-1);
    }
  }

  sleep(5);// 等待 5 秒
  // 分配任务的循环
  for(t=1;t<=4;t++){
    pthread_mutex_lock(&g_task_lock);// 加锁
    tail+=t;// 增加任务数
    printf("I am Boss, I assigned %d tasks, I notify all coders!\n", t);
    pthread_cond_broadcast(&g_task_cv);// 通知所有等待的线程有新任务
    pthread_mutex_unlock(&g_task_lock);// 解锁
    sleep(20);// 等待 20 秒
  }
  // 通知线程退出
  pthread_mutex_lock(&g_task_lock);
  quit = 1;// 设置退出标志
  pthread_cond_broadcast(&g_task_cv); // 通知所有线程
  pthread_mutex_unlock(&g_task_lock);
  // 销毁锁和条件变量
  pthread_mutex_destroy(&g_task_lock);
  pthread_cond_destroy(&g_task_cv);
  pthread_exit(NULL);// 主线程退出
}

核心逻辑:

  • 线程等待任务: 每个线程运行 coder 函数,先检查任务队列是否有任务。如果没有任务,线程就等待(通过条件变量 g_task_cv)。
  • 主线程分配任务: 主线程增加 tail 值,模拟新任务的生成,并通知所有等待的线程。
  • 任务执行和退出: 工作线程被唤醒后取任务执行,当主线程设置 quit 标志时,所有线程被通知退出。

day2任务是将线程的两个例子手动实现一下,然后以进程数据结构为基础写一篇blog

  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值