并发编程
前瞻
并发编程应用于一些需要多任务执行的项目,由于进程间通信往往比线程通信麻烦,因此,多任务应该优先考虑使用多线程。
在Linux扩展应用中,常常需要调用别人写好的函数,经过我们自己对基础的底层函数的熟练掌握,会对别人的函数的实现有一个大体的模型感。通过学习中不断查看man手册,对别人的readme读取的效率也会自然变高。
程序、进程、线程的想象和理解
理解
- 首先前面解除的程序的理解要明确,程序可以说是一段写好的代码,存放在硬盘里面那种。
然后进程就是进行中的调度程序来执行的空间。可以说,程序是菜谱,进程就是炒菜的过程。
而一个进程可以有多个线程,也就是说我可以同时炒菜和煲汤。炒菜和煲汤共享的都是一个厨房(资源共享)
想象:
- 我觉得可以将子进程想象成多元宇宙,创建一个子进程就是你分裂出一个多元宇宙,生成了一个一模一样的厨房,但是他炒的可能和你不一样的菜。
做法:
- 你可以通过主动让自己脑中蹦出一个想法,无需实施,但是因为你的想法,多元宇宙中分裂了一个你去实施那个想法,等到时机成熟,利用你可以随意穿越多元宇宙的能力,去杀掉那些“你”,获取他的劳动成果。分裂出来的那个你,会继承你的所有记忆,也就是你的想法他都知道,但是由于创造它的时候会分配一个想法给他,因此它会向着那个想法去做(pid)。
题外话:
幸好他是个程序,没有思维,要是他是个人,继承了你的所有记忆,那他也会穿越从而造成多元宇宙混战。
多进程
意义:
- 多进程在并发编程中提供了相对独立的环境。
理解:
- 多进程的优势是独立安全,大部分通信机制都自带有原子性,劣势是占用资源较大。因此使用多进程,更多考虑和学习他的数据传输方式。
- 多线程通信几乎没有原子性,因此更多考虑使用各种上锁或者信号量解决数据原子性的问题
- 多线程通过传递参数或者全局变量拿到同样的id,进行通信,多进程通过约定一样的参数生成同样的key和id,进行通信。
典例: - 浏览器标签页,每个标签应该创建一个进程,即使一个标签崩溃,不会影响其他标签页
- 多任务系统,不同会话不同进程,保证了数据安全和独立
- 守护进程,需要较高的可靠性和独立性
进程的阶段
类似人生的阶段
- 出生阶段:被fork()叉子函数生出来就进入就绪状态
- 就绪状态:相当于饭馆拿到号,正在等位置,等待schedule来引导你到你的位置吃饭,但是有很多人排队,有普通人,有VIP插队的人。吃饭到一半时可以进入睡眠状态和暂停(调试)状态,回来时将重新等待schedule引导。
- 执行状态:正在被CPU处理,相当于正在吃饭。
- 僵尸态:吃完饭,死了,变成一具尸体(欢迎来验)
- 死亡态:验完尸了,判定好了死亡原因,后事料理完毕。
优雅退出进程方案atexit(func)、exit和_exit
意义:
- 提供了独立安全全面的退出进程的方案
理解:
- 如果多个地方使用exit,不用分别在多个地方在exit前面写释放函数,而且代码看起来更简洁更结构化。
操作
- 配合atexit(func)可以在exit调用时处理func,一般这个func可以用来善后。另外exit还可清缓冲区。而_exit只是退出,没有善后和清缓冲的功能。
生成多进程的方案fork、exec和vfork
意义:
- 提供多样化的多线程的启动方案
理解:
- 前面说到,我们一般不希望多元宇宙中的其他自己的思想和自己一样。
因此在产生分裂的时候,我们可以设计一套记忆和思维给他,让他成为一个真正的傀儡。fork产生子进程后,exec即植入思维,子进程将不会继承我的任何东西,而是将exec给的参数植入记忆。
1 |
|
exec函数簇
execl : 最毛坯版,直接把所有参数写在参数中,适合临时或者轻量场景
execv :优雅版,将参数写在字符串数组中,还可以用argv,提高了扩展性
execvp:优雅+非常方便版,可以因为环境变量中有而第一个参数只需要写应用名即可,但是移植性可能比优雅版降低,因为不同环境中环境变量不一样。
疑问:
- 这个是为了方便吗?这样完全继承不了我的任何东西,不就和新开一个程序一模一样了吗,为什么不用多线程呢?
某些时候,我们希望额外开一个完全不一样的工作,而不影响主线程代码的内容,保持内容类型一致性。使用exec前,记得将继承得到的需要释放的资源全部释放
vfork
fork的一个特殊版本,创建的子进程会共享空间,且会让父进程挂起,退出之后才会回到父进程。
疑问:
- 看起来完全就是为了给父进程做重要初始化的操作的,这和信号中的pause有什么区别?
这里父子进程是先后运行的,并不是并发进行,所以共享父进程的内存空间。vfork可以在非常省空间的情况下,配合exec簇立即跳到需要执行的内容。
wait和waitpid
意义:
- 提供个性化验尸方案,获得子线程的死亡原因
操作:
- 等待收尸函数,我个人觉得叫验尸函数比较精准,在前面我学会了几种杀死傀儡(子进程)的方法:对傀儡植入自杀想法(exit)、发送信号杀死等,傀儡死亡后,我希望知道他是怎么死的,因此我要拿一个棺材(wait的参数)来将它的尸体装回到我的宇宙
wait:阻塞型,哪个傀儡死就给哪个验尸。
waitpid: 可选择非阻塞,可选择傀儡号来验尸
总结:waitpid需要出一口棺材和两个选择,前者只需要出棺材,另外,wait只会在有子进程时等待,如果没有子进程会立即返回-1
1 | pid_t status ; //创建一口棺材 |
从棺材中装回的尸体(status)中,我们可以验到不少,它是一个4字节。
最低位的一个字节存放了exit值。头文件提供了这么些验尸工具:
1. WIFEXITED(status):如果子进程正常退出,则该宏为真。
2. WEXITSTATUS(status):如果子进程正常退出,则该宏将获取子进程的退出值。
3. WIFSIGNALED(status):如果子进程被信号杀死,则该宏为真。
4. WTERMSIG(status):如果子进程被信号杀死,则该宏将获取导致其死亡的信号值。
5. WCOREDUMP(status):如果子进程因信号死亡且生成核心转储文件(core dump),则该宏为真。
6. WIFSTOPPED(status):如果子进程的被信号暂停,且option中WUNTRACED已经被设置,则该宏为真。
7. WSTOPSIG(status):如果WIFSTOPPED(status)为真,则该宏将获取导致子进程暂停的信号值。
8. WIFCONTINUED(status):如果子进程被信号SIGCONT重新置为就绪态,该宏为真。
- 当我们不关心进程死亡原因时,可以利用孤儿进程可以被自动收养的特点,让子进程一出生就再生一个孙进程,然后子进程马上自杀。并且父进程马上验尸。这样,父进程就不用一直要想着给孙进程验尸(影响工作效率),将它交给init进程释放。
进程间通信IPC(signal和sigqueue)
- 传统通信
- 无名管道,需要内核中转,效率比较低,只适合轻量级传输
- 有名管道,和无名管道一样
- 信号,异步操作,不阻塞,可以提高线程的效率,使用pause阻塞还能用来等待重要操作
- System V IPC对象
- 共享内存,适合大量的数据传输
- 消息队列
- 信号量
- BSD
- 套接字
无名管道(存放于内存中,出栈即释放)
意义:
- 给血亲进程间提供了非常轻量化的通信
特点
读写分开,区别于读写端一致的一般文件
无名,只有亲缘之间可以使用,出栈就释放
不保证原子性,只能一对一
每个数据只有一份,只能被读一次
操作创建一个匿名管道需要给出一个描述符数组,两个描述符一个用来读,一个用来写
为了避免浪费描述符,读的那方应该释放写描述符,写的释放读描述符
疑问:
- 管道既然是一个文件,那么我们为什么不能用普通文件进行通信呢?
对于亲缘进程来说,无名管道更方便。对于陌生进程来说,有名管道有原子性,多个文件对管道读写时保证 不会撞车。 - 什么时候需要设置非阻塞?
看具体需求,由于读的时候如果有写者,没有内容会默认阻塞等待,如果希望是没内容也直接离开,就可以设置非阻塞。
如果写的时候没有读者,希望等待有读者,而不是直接被杀死,也要设置。
读写时,会有如下几种情况
读操作
| 有写者 | 无写者 | |
|---|---|---|
| 有数据 | 正常读出 | 正常读出 |
| 无数据 | 阻塞等待 | 立即返回0 |
写操作
| 有读者 | 无读者 | |
|---|---|---|
| 缓冲区已满 | 阻塞 | 立即收到信号SIGPIPE |
| 缓冲区未满 | 正常写入 | 立即收到信号SIGPIPE |
有名管道(存放在磁盘中,不需要重复创建)
意义:
- 为陌生进程提供了自带原子性的轻量化通信
特点
- 原子性,可以多进程间同时读写
- 有名,陌生进程可以通信
- 即时性,管道中的数据,如果及时不读,就会消失或者连续乱接
典例:
- 利用原子性,多个陌生进程的log可以通过管道中转存储到一个log文件中。
- 利用有名性,两个进程之间可以通过管道发送字符。
信号(异步)
意义:
- 进程最为轻量级的通信,提供了异步逻辑,让通信不过多影响进程本身的工作。
特点:
- 异步,提前注册然后去做别的事,不必等待,提高工作效率。
- pause机制,可以等待信号,用于init类型的重要函数。
想象:
- 异步信号可以想象冬日战士,我(发信号的线程)可以穿越多元宇宙(其他进程)给其他冬日战士发送指令。当然,这些冬日战士出厂就默认有那么一套刻在他基因里面的特殊流程(sigaction),他可能在做自己的事,但是一旦听到信号,巴基就会变成冬兵,执行我的任何命令,包括自杀。
细节:
- 一般来说,进程会有4种响应模式,在冬兵基因(子进程代码)中设置好,分别是阻塞(暂时屏蔽)block、捕获catch、丢弃(忽略)ignore、却省default。通过signal函数可以给单个信号设置捕捉,忽略,缺省三种,如果要设置阻塞,需要将线程中自带的信号屏蔽集sigblock设置一下,这个屏蔽集是线程私有的,且默认是空。
特殊地:kill和stop信号无法被编写。
signal和kill
一个负责注册信号的响应机制和响应函数,一个负责发送信号,配套使用。
但是用这个方法的响应函数只能携带一个int值,就是信号类型编号,因此,这种信号传输无法携带任何自定义的内容。
sigqueen和sigaction
pause
暂停等待对信号的响应。只有在缺省和捕获两种情况下收到信号才能解除,忽略和阻塞都无法解除。
信号集
当一个傀儡(进程)收到信号时,信号被存放在signals指针指向的sigset中,而傀儡本身也有一个名叫sigblock的sigset。傀儡响应时,会根据block忽略掉一些信号,没有忽略的信号会被拿出来,根据sigaction指向的表中的内容去完成所做的事。这个sigblock就是一个可以自定义的信号集。
补充
- 段错误如果自定义,会一直给你发信号,并不会退出你的主进程,但是如果是abort()就会执行服务函数后退出。
1 | 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP |
System V
意义:
- 提供进程间更精准、更快速、更重量级的通信机制
key
意义:
- 为每个不同的通信方式和通信圈得到一个专属钥匙,防止串台
key通过给出两个参数产生,不同的IPC应该使用不同的KEY,即便它们在同一个进程中进行
消息队列 message queen
意义:
- 提供了比较轻量原子性的进程通信方式,可以控制权限,比较适合小数据量多对多精准通信
疑问:
- Ftok创建key值,似乎是为了大数据量传输而准备的,是共享的,那么对比有名管道如何,还能做到原子性吗,是不是用起来比管道还要麻烦呢?
是加强版的有名管道,有原子性,还有取件码呢!用起来并不麻烦,感觉完胜有名管道呀。。相比管道,可以多对多,一种加强版的管道。
特点:
- 菜鸟驿站,可以存放且需要一个特征码才能获取
- 原子性,可以多对多同时进行
- 可以通过msgctl限制用户和进程的访问权限,比较安全
使用方法:
- ftok得到key:根据目录和自定义整型生成唯一的key(相当于菜鸟驿站的位置和一个自定义编号,一样的参数会得到一样的key值)
- msgget将key转换成msqid : 根据key生成一个唯一店铺编号,key一样的情况下,转换得到的msqid也一样,这个msqid是内核给出的,相当于前面的fd,此时消息队列已经被创建出来,可以使用命令ipcs -a查看
- 发送消息:使用msgsnd发送消息,并指定一个第一个参数为long的结构体作为消息结构体发送
- 接收消息:使用msgrcv接收消息,并给出同样的结构体接收,还有long数据作为一个接收号
- 删除队列:使用msgctl删除一个队列
可能的误解:
- 由于生成key值使用了路径,可能会误以为这个消息队列根据key值会创建在那个路径下,其实并非这样,消息队列是内核空间里的,不是一个用户能够操作的文件。
共享内存
意义:
- 提供了进程间方便高速的数据共享,无缓冲,适合大数据量,但是没有权限制度,需要配合信号量提供原子性。
疑问:
共享内存的ID获取似乎不是非常严格,可以随机产生一个KEY,但是如果随机生成就不能复制代码了拿到同样的KEY了,对吗?
并非如此,通过同样的参数(路径和自定义int)创建同样的key,通过同样的key也会创建同样的shmid(如果shmget第二个参数不一样,也不会覆盖第一次创建的shmid,所以同样的代码得到的shmid一定是一样的。在进行映射的时候,shmat的第二个参数是NULL(一般情况),得到的虚拟内存的首地址将会不一样,但是他们对应的真实物理地址是一样的,所以即便两个进程获得不一样的首地址,也可以互相通信。系统页存储是什么?为什么共享内存的尺寸最好要是它的整数倍?
共享内存在被创建时,分配的是虚拟内存,获得虚拟内存的首地址。由于系统硬件需要管理这一块虚拟内存,他们之间的交互是按页交互的,因此共享内存的尺寸需要是页的整数倍。共享内存的数据不安全体现在哪?共享内存的原子性如何?
共享内存并不能设置谁能读谁能改,而且共享内存本身没有原子性,不安全,因此需要通过信号量达到原子性。
特点:
- 非常适合大量数据的进程间传输
- 几乎没有自带的原子机制,需要配合信号量使用
信号量
意义:
- 提供了陌生进程间原子性的数据修改机制
疑问:
- 修改数据时候互斥,那不就是原子性吗?是不是共享内存修改的原子性不好,信号量能很好解决问题?我为什么不直接使用一个整型的加减来解决呢?
是的,信号量是CPU支持的一个具有原子性的操作,使用整型的加减是没有原子性的,同时操作可能会冲突。信号量可以通过原子性特点,为共享内存提供原子性支持。
共享内存+信号量配合的操作过程
- 通过不同的key值对两个IPC获得两个id,分别是shmid和semid,其中,在使用semget获取id时,第二个参数指定了有几个量,(这里是数据和空间两个量所以写2)
- 使用semctl初始化那两个量的值。
1 |
|
- 在申请车位进入停车场之前,定义两个操作结构体sembuf(如果只有车+,车位-两种操作),将结构体放到数组中传递给semop
线程
意义
- 提供非常简单高效的并发需求
前瞻
线程是内核调度的最小单位,进程是资源分配的最小单位
也就是说,对于内核调度来说,它只看得见线程的PCB
疑问:
线程对比进程会有哪些优势?怎么考虑线程和进程的选择?当我有一个小任务要做,使用线程是不是更加轻量级,而不是开一个进程消耗更多资源?
是的,线程更加轻量,线程间通信可以直接传递参数,通信更方便,但是同时原子性较差,需要更多考虑同步互斥。总的来说,选择线程还是进程需要考虑:通信的频繁性和安全性,资源的使用量,稳定性。线程释放起来是不是更加方便?
释放起来对比进程会更方便,需要返回值情况下,需要任意个线程阻塞等待回收,不需要返回值直接设置分离即可。进程则需要父进程回收(可非阻塞),或者通过fork两次,产生孤儿进程的操作。主函数退出意味着进程终止,所有非分离线程都会一起终止吗?
不是,主线程退出有几种情况
Return和exit:终止整个进程,包括还在进行的非分离线程。
pthread_exit : 只是终止线程,而不会影响其他线程,p_exit的空间会释放,不应该被访问。
使用过程和概念
- 创建p_creat,用户给出id地址和服务函数。
主线程和其他线程地位是平等的,叫主线程只是因为他的函数是main而已,因此它p_exit时并不会影响其他线程,其他线程调用exit也会将主线程在内的所有线程干掉。因此,不要因为平时都在主线程里面做事就误以为主线程是不能死的,我完全可以主线程一生成另一条线程,就让主线程死掉,把生出来的那条线程当成主线程用。
- 退出p_exit,由线程自己调用退出,对比exit退出整个进程
当我们需要返回值时,应当使用非分离线程,可以接受返回值.p_exit函数参数可以传递一个任意指针类型。某个线程中使用p_join指明阻塞回收的id和给出一个void** 来接收这个地址。但是这里只能阻塞等待收尸。另外,如果线程在分离之前就被join找到的话,join也是可以拿到分离进程的退出内容的,只不过这不太合理罢了。
分离p_detach,将自己分离,当不需要返回值时,将自己分离是一个比较合适的做法,因为这样不必找一个人阻塞给他收尸
标准错误处理,由于线程很多,全局变量并没有原子性,因此不能使用errno全局变量,即perror并不能使用。而线程函数的返回值都是局部变量,可以直接使用strerror()来打印错误值
1 | int err = pthread_creat(p_id,NULL,func,NULL); |
线程的取消
意义:
- 提供了动态释放一条线程的方案
默认情况下,p_cancel让线程在取消点函数到来时p_exit.线程内部可以设置为屏蔽cancel信号和收到信号立即cancel
1 | pthread_setcanceltype(PTHREAD_CANCEL_DISABLE,NULL);//屏蔽取消信号 |
线程退出清理p_cleanup_push_pop
意义:
- 提供了一个防止线程退出时一些必要工作没完成的方案
一般会有两种情况会用到这对函数
- 有需要释放的资源,比如堆空间,句柄等
- 锁还没还,退出可能造成死锁
机制
- pthread_cleanup_push压进一个void ()(void)函数,这个函数将会在p_exit和p_cancel到来时执行压入的函数
- pthread_cleanup_pop(0或1)当执行到这一行时,0不执行只弹出,1弹出并执行退出函数
操作
- 使用pthread_cleanup_pop(1)在最后,保证退出函数一定能执行(最简单最常用)
- 使用pthread_cleanup_pop(0)应该不是放在最后,而是一些具体特殊的逻辑
函数单例
意义:
- 提供了多线程下精准初始化的操作
过去,我们一般在主线程下进行初始化,这样会占用主线程的执行时间,也就是说,在还没有到使用这些初始化量的时候就话时间将其初始化。函数单例可以让多线程被创建时再进行初始化,单例在运行时会阻塞其他所有调用这个p_once的线程,但是这个初始化函数只能是无参无返回值,因此需要全局或者静态变量给它初始化
1 | pthread_once_t once = PTHREAD_ONCE_INIT; |
互斥 mutual exclusion 和 读写锁 read write lock
意义:
- 可以让数据拥有原子性,读写锁可以让读的效率更高
互斥即一把锁,只要拿到锁,再有人要拿,只能阻塞等待归还。
读写锁即有无数把读锁,只有一把写锁,而且需要拿写锁时,读锁必须全部已经归还。
读写锁是互斥的增集,因为它可以提高读的效率,而写的效率和互斥一样。
POSIX匿名信号量
意义:
- 提供更方便的信号量的接口
优势和经典模型:
- 当信号量值为1时,就可以充当互斥锁的作用。
- 可以控制并发的数量,解决生产者-仓库-消费者问题,即仓库有限,生产者等待有空间再生产,消费者等待有货物再消费,空间和货物就是两个信号量。
- 可以制作轻量级的线程池,比使用条件变量+互斥的线程池精细度低,只要有信号,就会唤醒线程。
操作:
- 申请一个sem_t就是一个信号量,然后sem_init初始化它的量,使用sem_wait_post进行p和v操作。
条件变量和锁的配合
意义:
- 让线程有任务运行,没任务就睡觉。
疑问:
- 条件量的和锁的配合比单独用锁,起到什么作用?
思考一个模型:红警2中的矿场的采矿模型。
如果只使用互斥锁的情况下,矿车没有休眠和被唤醒的操作,那么当金子没有时矿车设置为全部结束,再也无法启动。显然不合理。
但是如果我们有了条件量+互斥情况下,我们可以让没矿时,矿车设置休眠,当矿自然产生达到一定量时,通知并唤醒所有矿车。
优势和典例
- 非常适合制作高精细度的线程池
线程池
疑问:
- 就是将任务放到链表中,让多个线程去拿取链表中的任务执行,使用的是前面的条件变量+互斥的模型吗?如何判断和拿到任务的地址?
是的,线程池是条件变量+互斥+队列的模型,将不同的函数指针放入队列中。当线程执行任务的时候,出队节点且执行那个函数。此外,可以在节点中插入时间片信息。同时将节点作为函数参数,让任务函数拿到时间片,根据时间片控制自己的退出时间,达到定时退出的效果。
线程的分离与结合
意义:
- 可以让线程完全无痛去世。当我们对线程的返回值不感兴趣时,我们应该将其分离。
操作:
- 线程可以在创建时,通过传入一个attr来设置分离,也可以在线程中使用p_detach(p_self())来设置自己为分离,当然也可以设置别人分离,只要你有id号,使用attr需要初始化和宏设置,还要释放attr,比较麻烦。
1 | //创建时设置 |
当我们在分离前使用p_join结合了这条线程,即便后面分离,也能拿到退出值。但是好像没有什么意义。。
优先级和调度策略
意义:
- 线程执行优先策略,提供了更高级更精细的控制多线程工作逻辑方案,让忙碌的CPU能先执行更重要的任务。
疑问:
- 进程的优先级和线程优先级都是将逻辑上重要的任务排到前面吗?类似读写中,一般写的优先级设置得比读更高
是的,进程优先级决定了操作系统调度程序在何时调度一个进程。优先级高的进程会比优先级低的进程获得更多的 CPU 时间。
理解:
- 首先,实时调度FIFO、RR线程看的是静态优先级,静态优先级一旦确定就无法更改,为1~99.这个值越高优先级越高。
- 非实时调度OTHER看的是动态优先级,动态优先级会受nice值和cpu调度习惯所影响,nice值越低,越能提高动态优先级。
- 进程默认创建时为非实时调度线程,策略为OTHER。
- 实时线程运行时,非实时进程将无法执行,也就是说可以将非实时进程的静态优先级看成0
- FIFO的进程一旦拿到CPU,将一直占用直至退出,RR的进程拿到CPU时,在时间片消耗完后会让出给同等静态优先级的其他实时线程使用。
典例
- 前台任务和后台任务,前台任务应该是立即执行,而后台任务应该是CPU空闲时执行的
- IO密集型任务,比如网络IO可以先保证传输再进行运算
- RR:汽车的ABS防抱死制动,需要在规定的时间内执行,否则将会退出交由驾驶员操作
操作:
- 使用p_attr_setinheritsched声明要显式设定策略
- p_attr_setschedpolicy设定调度策略
- 设定静态优先级
- 将&attr创建线程时放入参数2
1 | pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);//默认继承,这里显示声明要自定义 |
问题:
- 在不同的系统中,可能会出现这样的问题,主线程的调度优先级最高,导致无论怎么设置子线程的优先级,都会导致子线程无法运行,除非缺省。
- 还有的系统即便设计了FIFO还是会将CPU让出一部分?????
比如我下面的代码,按道理来说应该只打印a,但是有的电脑直接不打印pause(),有的a和b交替打印
1 |
|
动静态库
意义:大大提高开发效率
编译过程
- 预处理:宏替换、头文件展开
- 编译:c转为汇编
- 汇编: 汇编转换为2进制
- 链接:与库文件链接,将需要的内容重定位
静态库archive(只卖不借的library)
意义:
- 提供不受平台约束,方便移植的内容
一个.a静态库就是多个.o文件的集合,可以使用ar命令,t、d、r、x 分别是查看删除和替代复制.a中的.o文件
而且,检索是顺序的,一旦检索到需要的内容,后面就不再检索这个内容,所以,函数的重写是没有用的
当库之间有依赖关系时,编译时前面的参数为依赖者,后面的参数为被依赖的库。
具体步骤
- 将源码编译成.o
1 | gcc -c a.c -o a.o |
- 将.o合并为一个archive
1 | ar crs libxxx.a *.o |
- 使用时
1 | gcc main.c -L. -lxxx -o main |
指令细节
- -o +目标文件名可以永远放到最后写
- ar指令中crs必须放在归档文件和输入文件前面,不同于动态库gcc合并
动态库share object(只借不卖的library)
意义:
- 提供利于迭代,节省存储空间的方案
命名规则
- 在动态库的管理中,通常会使用符号链接(软链接)来管理不同版本的库,常见的策略为
• libdd.so.1软链接libdd.so.1.0.0。• libdd.so软链接libdd.so.1
• libdd.so.1.0.0:实际的库文件
每次小范围更新库文件,手动更新符号链接ln -sf libdd.so.1.0.1 libdd.so.1
如果大范围更新库文件,甚至不再支持过去老代码了,将会将so 链接到so.2
具体步骤
- 将源码编译为.o
1 | gcc -c -fPIC a.c -o a.o |
- 将.o文件添加成.so文件
1 | gcc -shared -fPIC *.o -o libxxx.so |
编译文件时,指定-L. -lxxx 即文件夹和动态库名字
为了让我们编译时能够成功找到动态库,可以
- 在编译文件时指定-Wl,-rpath=/path
- 设置环境变量export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path
Makefile
意义:一个高级脚本,可以大大提高编译效率
组成:
- 每一个块成为一条规则,每一条规则包括目标:依赖\n\t指令
逻辑:
- 只有一个最终目标(make的参数,缺省默认第一个),找到所有依赖,开始执行指令
- 如果某些规则虽然有目标,但是不是最终目标,也不是依赖,将被忽略
- 执行规则时,依赖文件比目标文件时间戳更新,才会执行下面的指令
- 即便依赖不是真正目标文件需要的,只要在依赖列表,都必须被找到
操作:
- 可以通过伪目标,达到多文件编译的效果,需要声明伪目标,防止同名.c早成隐式指令
1 | fake_target: a b |
- 可以利用make的隐式规则,偷懒
1 | fake_target: a b |
当make发现a和a.c为同名文件且形式是依赖,将会隐式将空指令改为cc a.c -o a
- 自定义变量,Makefile中的变量默认是不以空白符结尾的字符串
1 | VALUE = a b |
- 内置变量,是全局变量,+=可以让继承的内容不被擦掉,顶层自定义变量也是全局变量
1 | CFLAGS += "默认空,一般用来放" |
- 当使用 = 赋值时,是全文搜索等号右边值的,右值可以在文章下面,不希望全文搜索可以使用:=
- 通配符%的使用
1 | A = 1.txt 2.txt 3.txt 4.c |
- 在命令前面使用@为静默执行,不会将命令输出到控制台
- 在命令前面使用-为失败不阻塞,即失败了不会影响后面的执行
override
意义:
- 可以让程序员临时在make时链接库而不覆盖原来的
详细:
- 当我们make LDFLAGS=”-ltem”时,系统将会将这句放到makefile中所有LDFLAGS赋值的前面,后面没有override关键字的LDFLAGS都会被注释掉
操作:
- 如果我们希望原来的和临时的都保留,应该原来的添加override关键字且是+=号
典例:
- 我们常常希望打开错误提示,因此makefile中一般有这一句:override CFLAGS += -Wall
静态规则
意义
- 将大量的规则使用通配的方法合并为1条规则
做法:
1 | BIN=a b |
这里将所有无后缀文件:同名.c,也可以C = $(BIN):%=%.c即将所有无后缀文件名改名为同名.c放入C中,当然不是真的改它,只是一个字符串处理而已
内置函数
意义:
- makefile里面能用的指令不多,字符串内置函数可以精细化处理一些字符需求
1 | SRC=$(wildcard *.c)#匹配目录下所有的.c |
嵌套Makefile
意义:
- 让make可以文件逐级编译,每层都有自己的Makefile
1 | all: $(BIN) |
LVGL
意义:
- 为Linux提供比较轻量优美的前端框架



