前瞻

并发编程应用于一些需要多任务执行的项目,由于进程间通信往往比线程通信麻烦,因此,多任务应该优先考虑使用多线程。

在Linux扩展应用中,常常需要调用别人写好的函数,经过我们自己对基础的底层函数的熟练掌握,会对别人的函数的实现有一个大体的模型感。通过学习中不断查看man手册,对别人的readme读取的效率也会自然变高。

程序、进程、线程的想象和理解

理解

  • 首先前面解除的程序的理解要明确,程序可以说是一段写好的代码,存放在硬盘里面那种。
    然后进程就是进行中的调度程序来执行的空间。可以说,程序是菜谱,进程就是炒菜的过程。
    而一个进程可以有多个线程,也就是说我可以同时炒菜和煲汤。炒菜和煲汤共享的都是一个厨房(资源共享)

想象:

  • 我觉得可以将子进程想象成多元宇宙,创建一个子进程就是你分裂出一个多元宇宙,生成了一个一模一样的厨房,但是他炒的可能和你不一样的菜。

做法:

  • 你可以通过主动让自己脑中蹦出一个想法,无需实施,但是因为你的想法,多元宇宙中分裂了一个你去实施那个想法,等到时机成熟,利用你可以随意穿越多元宇宙的能力,去杀掉那些“你”,获取他的劳动成果。分裂出来的那个你,会继承你的所有记忆,也就是你的想法他都知道,但是由于创造它的时候会分配一个想法给他,因此它会向着那个想法去做(pid)。

题外话:
幸好他是个程序,没有思维,要是他是个人,继承了你的所有记忆,那他也会穿越从而造成多元宇宙混战。

多进程

意义:

  • 多进程在并发编程中提供了相对独立的环境。

理解:

  • 多进程的优势是独立安全,大部分通信机制都自带有原子性,劣势是占用资源较大。因此使用多进程,更多考虑和学习他的数据传输方式。
  • 多线程通信几乎没有原子性,因此更多考虑使用各种上锁或者信号量解决数据原子性的问题
  • 多线程通过传递参数或者全局变量拿到同样的id,进行通信,多进程通过约定一样的参数生成同样的key和id,进行通信。
    典例:
  • 浏览器标签页,每个标签应该创建一个进程,即使一个标签崩溃,不会影响其他标签页
  • 多任务系统,不同会话不同进程,保证了数据安全和独立
  • 守护进程,需要较高的可靠性和独立性

进程的阶段

类似人生的阶段

  1. 出生阶段:被fork()叉子函数生出来就进入就绪状态
  2. 就绪状态:相当于饭馆拿到号,正在等位置,等待schedule来引导你到你的位置吃饭,但是有很多人排队,有普通人,有VIP插队的人。吃饭到一半时可以进入睡眠状态和暂停(调试)状态,回来时将重新等待schedule引导。
  3. 执行状态:正在被CPU处理,相当于正在吃饭。
  4. 僵尸态:吃完饭,死了,变成一具尸体(欢迎来验)
  5. 死亡态:验完尸了,判定好了死亡原因,后事料理完毕。

优雅退出进程方案atexit(func)、exit和_exit

意义:

  • 提供了独立安全全面的退出进程的方案

理解:

  • 如果多个地方使用exit,不用分别在多个地方在exit前面写释放函数,而且代码看起来更简洁更结构化。

操作

  • 配合atexit(func)可以在exit调用时处理func,一般这个func可以用来善后。另外exit还可清缓冲区。而_exit只是退出,没有善后和清缓冲的功能。

生成多进程的方案fork、exec和vfork

意义:

  • 提供多样化的多线程的启动方案

理解:

  • 前面说到,我们一般不希望多元宇宙中的其他自己的思想和自己一样。
    因此在产生分裂的时候,我们可以设计一套记忆和思维给他,让他成为一个真正的傀儡。fork产生子进程后,exec即植入思维,子进程将不会继承我的任何东西,而是将exec给的参数植入记忆。
1
2
3
4
5
6
7
8
9
10
11
12

if( pid > 0 )
{
printf("这里是父进程会进入的地方\n");
}
if (pid == 0)
{
//将我设计好的思维植入我的傀儡
execl("./Thinking","./Thinking",NULL);
//后面的代码将不会出现在子进程,因此hello永远不会被打印.
printf("hello\n");
}

exec函数簇

execl : 最毛坯版,直接把所有参数写在参数中,适合临时或者轻量场景
execv :优雅版,将参数写在字符串数组中,还可以用argv,提高了扩展性
execvp:优雅+非常方便版,可以因为环境变量中有而第一个参数只需要写应用名即可,但是移植性可能比优雅版降低,因为不同环境中环境变量不一样。

疑问:

  • 这个是为了方便吗?这样完全继承不了我的任何东西,不就和新开一个程序一模一样了吗,为什么不用多线程呢?
    某些时候,我们希望额外开一个完全不一样的工作,而不影响主线程代码的内容,保持内容类型一致性。使用exec前,记得将继承得到的需要释放的资源全部释放

vfork

fork的一个特殊版本,创建的子进程会共享空间,且会让父进程挂起,退出之后才会回到父进程。

疑问:

  • 看起来完全就是为了给父进程做重要初始化的操作的,这和信号中的pause有什么区别?
    这里父子进程是先后运行的,并不是并发进行,所以共享父进程的内存空间。vfork可以在非常省空间的情况下,配合exec簇立即跳到需要执行的内容。

wait和waitpid

意义:

  • 提供个性化验尸方案,获得子线程的死亡原因

操作:

  • 等待收尸函数,我个人觉得叫验尸函数比较精准,在前面我学会了几种杀死傀儡(子进程)的方法:对傀儡植入自杀想法(exit)、发送信号杀死等,傀儡死亡后,我希望知道他是怎么死的,因此我要拿一个棺材(wait的参数)来将它的尸体装回到我的宇宙

wait:阻塞型,哪个傀儡死就给哪个验尸。
waitpid: 可选择非阻塞,可选择傀儡号来验尸
总结:waitpid需要出一口棺材和两个选择,前者只需要出棺材,另外,wait只会在有子进程时等待,如果没有子进程会立即返回-1

1
2
pid_t status ;  //创建一口棺材
waitpid( pid , &status , WNOHANG);//不阻塞型收尸

从棺材中装回的尸体(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)

  • 传统通信
  1. 无名管道,需要内核中转,效率比较低,只适合轻量级传输
  2. 有名管道,和无名管道一样
  3. 信号,异步操作,不阻塞,可以提高线程的效率,使用pause阻塞还能用来等待重要操作
  • System V IPC对象
  1. 共享内存,适合大量的数据传输
  2. 消息队列
  3. 信号量
  • BSD
  1. 套接字

无名管道(存放于内存中,出栈即释放)

意义:

  • 给血亲进程间提供了非常轻量化的通信

特点

  1. 读写分开,区别于读写端一致的一般文件

  2. 无名,只有亲缘之间可以使用,出栈就释放

  3. 不保证原子性,只能一对一

  4. 每个数据只有一份,只能被读一次
    操作

  5. 创建一个匿名管道需要给出一个描述符数组,两个描述符一个用来读,一个用来写

  6. 为了避免浪费描述符,读的那方应该释放写描述符,写的释放读描述符

疑问:

  1. 管道既然是一个文件,那么我们为什么不能用普通文件进行通信呢?
    对于亲缘进程来说,无名管道更方便。对于陌生进程来说,有名管道有原子性,多个文件对管道读写时保证 不会撞车。
  2. 什么时候需要设置非阻塞?
    看具体需求,由于读的时候如果有写者,没有内容会默认阻塞等待,如果希望是没内容也直接离开,就可以设置非阻塞。
    如果写的时候没有读者,希望等待有读者,而不是直接被杀死,也要设置。

读写时,会有如下几种情况

读操作

有写者 无写者
有数据 正常读出 正常读出
无数据 阻塞等待 立即返回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
2
3
4
5
6
7
8
9
10
11
12
13
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

System V

意义:

  • 提供进程间更精准、更快速、更重量级的通信机制

key

意义:

  • 为每个不同的通信方式和通信圈得到一个专属钥匙,防止串台

key通过给出两个参数产生,不同的IPC应该使用不同的KEY,即便它们在同一个进程中进行

消息队列 message queen

意义:

  • 提供了比较轻量原子性的进程通信方式,可以控制权限,比较适合小数据量多对多精准通信

疑问:

  • Ftok创建key值,似乎是为了大数据量传输而准备的,是共享的,那么对比有名管道如何,还能做到原子性吗,是不是用起来比管道还要麻烦呢?
    是加强版的有名管道,有原子性,还有取件码呢!用起来并不麻烦,感觉完胜有名管道呀。。相比管道,可以多对多,一种加强版的管道。

特点:

  1. 菜鸟驿站,可以存放且需要一个特征码才能获取
  2. 原子性,可以多对多同时进行
  3. 可以通过msgctl限制用户和进程的访问权限,比较安全

使用方法:

  1. ftok得到key:根据目录和自定义整型生成唯一的key(相当于菜鸟驿站的位置和一个自定义编号,一样的参数会得到一样的key值)
  2. msgget将key转换成msqid : 根据key生成一个唯一店铺编号,key一样的情况下,转换得到的msqid也一样,这个msqid是内核给出的,相当于前面的fd,此时消息队列已经被创建出来,可以使用命令ipcs -a查看
  3. 发送消息:使用msgsnd发送消息,并指定一个第一个参数为long的结构体作为消息结构体发送
  4. 接收消息:使用msgrcv接收消息,并给出同样的结构体接收,还有long数据作为一个接收号
  5. 删除队列:使用msgctl删除一个队列

可能的误解:

  • 由于生成key值使用了路径,可能会误以为这个消息队列根据key值会创建在那个路径下,其实并非这样,消息队列是内核空间里的,不是一个用户能够操作的文件。

共享内存

意义:

  • 提供了进程间方便高速的数据共享,无缓冲,适合大数据量,但是没有权限制度,需要配合信号量提供原子性。

疑问:

  • 共享内存的ID获取似乎不是非常严格,可以随机产生一个KEY,但是如果随机生成就不能复制代码了拿到同样的KEY了,对吗?
    并非如此,通过同样的参数(路径和自定义int)创建同样的key,通过同样的key也会创建同样的shmid(如果shmget第二个参数不一样,也不会覆盖第一次创建的shmid,所以同样的代码得到的shmid一定是一样的。在进行映射的时候,shmat的第二个参数是NULL(一般情况),得到的虚拟内存的首地址将会不一样,但是他们对应的真实物理地址是一样的,所以即便两个进程获得不一样的首地址,也可以互相通信。

  • 系统页存储是什么?为什么共享内存的尺寸最好要是它的整数倍?
    共享内存在被创建时,分配的是虚拟内存,获得虚拟内存的首地址。由于系统硬件需要管理这一块虚拟内存,他们之间的交互是按页交互的,因此共享内存的尺寸需要是页的整数倍。

  • 共享内存的数据不安全体现在哪?共享内存的原子性如何?
    共享内存并不能设置谁能读谁能改,而且共享内存本身没有原子性,不安全,因此需要通过信号量达到原子性。

特点:

  1. 非常适合大量数据的进程间传输
  2. 几乎没有自带的原子机制,需要配合信号量使用

信号量

意义:

  • 提供了陌生进程间原子性的数据修改机制

疑问:

  • 修改数据时候互斥,那不就是原子性吗?是不是共享内存修改的原子性不好,信号量能很好解决问题?我为什么不直接使用一个整型的加减来解决呢?
    是的,信号量是CPU支持的一个具有原子性的操作,使用整型的加减是没有原子性的,同时操作可能会冲突。信号量可以通过原子性特点,为共享内存提供原子性支持。

共享内存+信号量配合的操作过程

  1. 通过不同的key值对两个IPC获得两个id,分别是shmid和semid,其中,在使用semget获取id时,第二个参数指定了有几个量,(这里是数据和空间两个量所以写2)
  2. 使用semctl初始化那两个量的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define CAR 0
#define SPACE 1

void sem_init(int semid, int senum, int val)
{
union semun a ;
a.val = val;
semctl( semid, senum , a);
}

int main(void)
{
//初始化0台车和3个车位
sem_init(semid,CAR,0);
sem_init(semid,SPACE,3);
  1. 在申请车位进入停车场之前,定义两个操作结构体sembuf(如果只有车+,车位-两种操作),将结构体放到数组中传递给semop

线程

意义

  • 提供非常简单高效的并发需求

前瞻

线程是内核调度的最小单位,进程是资源分配的最小单位
也就是说,对于内核调度来说,它只看得见线程的PCB

疑问:

  • 线程对比进程会有哪些优势?怎么考虑线程和进程的选择?当我有一个小任务要做,使用线程是不是更加轻量级,而不是开一个进程消耗更多资源?
    是的,线程更加轻量,线程间通信可以直接传递参数,通信更方便,但是同时原子性较差,需要更多考虑同步互斥。总的来说,选择线程还是进程需要考虑:通信的频繁性和安全性,资源的使用量,稳定性。

  • 线程释放起来是不是更加方便?
    释放起来对比进程会更方便,需要返回值情况下,需要任意个线程阻塞等待回收,不需要返回值直接设置分离即可。进程则需要父进程回收(可非阻塞),或者通过fork两次,产生孤儿进程的操作。

  • 主函数退出意味着进程终止,所有非分离线程都会一起终止吗?
    不是,主线程退出有几种情况
    Return和exit:终止整个进程,包括还在进行的非分离线程。
    pthread_exit : 只是终止线程,而不会影响其他线程,p_exit的空间会释放,不应该被访问。

使用过程和概念

  1. 创建p_creat,用户给出id地址和服务函数。

主线程和其他线程地位是平等的,叫主线程只是因为他的函数是main而已,因此它p_exit时并不会影响其他线程,其他线程调用exit也会将主线程在内的所有线程干掉。因此,不要因为平时都在主线程里面做事就误以为主线程是不能死的,我完全可以主线程一生成另一条线程,就让主线程死掉,把生出来的那条线程当成主线程用。

  1. 退出p_exit,由线程自己调用退出,对比exit退出整个进程

当我们需要返回值时,应当使用非分离线程,可以接受返回值.p_exit函数参数可以传递一个任意指针类型。某个线程中使用p_join指明阻塞回收的id和给出一个void** 来接收这个地址。但是这里只能阻塞等待收尸。另外,如果线程在分离之前就被join找到的话,join也是可以拿到分离进程的退出内容的,只不过这不太合理罢了。

  1. 分离p_detach,将自己分离,当不需要返回值时,将自己分离是一个比较合适的做法,因为这样不必找一个人阻塞给他收尸

  2. 标准错误处理,由于线程很多,全局变量并没有原子性,因此不能使用errno全局变量,即perror并不能使用。而线程函数的返回值都是局部变量,可以直接使用strerror()来打印错误值

1
2
3
4
5
int err = pthread_creat(p_id,NULL,func,NULL);
if ( err ! = 0)
{
printf("pthread_creat ...:%s",strerror(err);
}

线程的取消

意义:

  • 提供了动态释放一条线程的方案

默认情况下,p_cancel让线程在取消点函数到来时p_exit.线程内部可以设置为屏蔽cancel信号和收到信号立即cancel

1
2
pthread_setcanceltype(PTHREAD_CANCEL_DISABLE,NULL);//屏蔽取消信号
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);//设置立即响应

线程退出清理p_cleanup_push_pop

意义:

  • 提供了一个防止线程退出时一些必要工作没完成的方案
    一般会有两种情况会用到这对函数
  1. 有需要释放的资源,比如堆空间,句柄等
  2. 锁还没还,退出可能造成死锁

机制

  • 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
2
pthread_once_t once = PTHREAD_ONCE_INIT;
pthread_once(once,Task);//Task必须是void (*)(void)

互斥 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
2
3
4
5
6
//创建时设置
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
pthread_t ptid;
pthread_creat(&ptid,&attr,func,NULL);

当我们在分离前使用p_join结合了这条线程,即便后面分离,也能拿到退出值。但是好像没有什么意义。。

优先级和调度策略

意义:

  • 线程执行优先策略,提供了更高级更精细的控制多线程工作逻辑方案,让忙碌的CPU能先执行更重要的任务。

疑问:

  • 进程的优先级和线程优先级都是将逻辑上重要的任务排到前面吗?类似读写中,一般写的优先级设置得比读更高
    是的,进程优先级决定了操作系统调度程序在何时调度一个进程。优先级高的进程会比优先级低的进程获得更多的 CPU 时间。

理解:

  • 首先,实时调度FIFO、RR线程看的是静态优先级,静态优先级一旦确定就无法更改,为1~99.这个值越高优先级越高。
  • 非实时调度OTHER看的是动态优先级,动态优先级会受nice值和cpu调度习惯所影响,nice值越低,越能提高动态优先级。
  • 进程默认创建时为非实时调度线程,策略为OTHER。
  • 实时线程运行时,非实时进程将无法执行,也就是说可以将非实时进程的静态优先级看成0
  • FIFO的进程一旦拿到CPU,将一直占用直至退出,RR的进程拿到CPU时,在时间片消耗完后会让出给同等静态优先级的其他实时线程使用。

典例

  • 前台任务和后台任务,前台任务应该是立即执行,而后台任务应该是CPU空闲时执行的
  • IO密集型任务,比如网络IO可以先保证传输再进行运算
  • RR:汽车的ABS防抱死制动,需要在规定的时间内执行,否则将会退出交由驾驶员操作

操作:

  1. 使用p_attr_setinheritsched声明要显式设定策略
  2. p_attr_setschedpolicy设定调度策略
  3. 设定静态优先级
  4. 将&attr创建线程时放入参数2
1
2
3
4
5
6
7
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);//默认继承,这里显示声明要自定义
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
struct sched_param param;
param.sched_priority = 99;//最高级且抢占能力最强
pthread_attr_setschedparam(&attr, &param);
pthread_t tid;
pthread_create(&tid, &attr, TASK, NULL);

问题:

  • 在不同的系统中,可能会出现这样的问题,主线程的调度优先级最高,导致无论怎么设置子线程的优先级,都会导致子线程无法运行,除非缺省。
  • 还有的系统即便设计了FIFO还是会将CPU让出一部分?????

比如我下面的代码,按道理来说应该只打印a,但是有的电脑直接不打印pause(),有的a和b交替打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <semaphore.h>

//设置一个线程服务函数
void* Task2(void* argue);
void* Task2(void* argue)
{
while(1)
{

fprintf(stderr,"%s",(char*)argue);

}

}
//设置一个线程服务函数
void* Task1(void* argue);
void* Task1(void* argue)
{


pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setinheritsched(&attr,PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedpolicy(&attr,SCHED_FIFO);
struct sched_param param;
param.sched_priority = 99;
pthread_attr_setschedparam(&attr,&param);
pthread_t ptid1;
pthread_create(&ptid1,&attr,Task2,"a");
while(1)
{

fprintf(stderr,"%s",(char*)argue);
}


}

int main(void)
{
//当一个优先级高的进程出现时,将会抢占优先级低的进程

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setinheritsched(&attr,PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedpolicy(&attr,SCHED_FIFO);
struct sched_param param;
param.sched_priority = 98;
pthread_attr_setschedparam(&attr,&param);
pthread_t ptid1;
pthread_create(&ptid1,&attr,Task1,"b");

while(1)
{
pause();
fprintf(stderr,"%s","c");

}


}

动静态库

意义:大大提高开发效率

编译过程

  • 预处理:宏替换、头文件展开
  • 编译: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软链接libdd.so.1
    
    • libdd.so.1软链接libdd.so.1.0.0。
    • 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 即文件夹和动态库名字

  • 为了让我们编译时能够成功找到动态库,可以

  1. 在编译文件时指定-Wl,-rpath=/path
  2. 设置环境变量export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path

Makefile

意义:一个高级脚本,可以大大提高编译效率

组成:

  • 每一个块成为一条规则,每一条规则包括目标:依赖\n\t指令

逻辑:

  • 只有一个最终目标(make的参数,缺省默认第一个),找到所有依赖,开始执行指令
  • 如果某些规则虽然有目标,但是不是最终目标,也不是依赖,将被忽略
  • 执行规则时,依赖文件比目标文件时间戳更新,才会执行下面的指令
  • 即便依赖不是真正目标文件需要的,只要在依赖列表,都必须被找到

操作:

  • 可以通过伪目标,达到多文件编译的效果,需要声明伪目标,防止同名.c早成隐式指令
1
2
3
4
5
6
7
8
9
10
11
fake_target: a b

a:a.c
gcc a.c -o a

b:b.c
gcc b.c -o b

clean:
rm -f a b
.PHONY: fake_target clean
  • 可以利用make的隐式规则,偷懒
1
2
3
4
5
6
7
fake_target: a b
#没有指令

a:a.c

b:b.c

当make发现a和a.c为同名文件且形式是依赖,将会隐式将空指令改为cc a.c -o a

  • 自定义变量,Makefile中的变量默认是不以空白符结尾的字符串
1
2
3
VALUE = a b

fake_target: VALUE
  • 内置变量,是全局变量,+=可以让继承的内容不被擦掉,顶层自定义变量也是全局变量
1
2
3
4
CFLAGS += "默认空,一般用来放"
LDFLAGS += "默认空,一般用来放库名"
CC = arm-linux-gcc # 默认gcc
CXX = arm-linux-g++ # 默认g++
  • 当使用 = 赋值时,是全文搜索等号右边值的,右值可以在文章下面,不希望全文搜索可以使用:=
  • 通配符%的使用
1
2
3
A = 1.txt 2.txt 3.txt 4.c
B = $(A:%.txt = %.md) #将会把所有.txt结尾改为.md结尾

  • 在命令前面使用@为静默执行,不会将命令输出到控制台
  • 在命令前面使用-为失败不阻塞,即失败了不会影响后面的执行

override

意义:

  • 可以让程序员临时在make时链接库而不覆盖原来的

详细:

  • 当我们make LDFLAGS=”-ltem”时,系统将会将这句放到makefile中所有LDFLAGS赋值的前面,后面没有override关键字的LDFLAGS都会被注释掉

操作:

  • 如果我们希望原来的和临时的都保留,应该原来的添加override关键字且是+=号

典例:

  • 我们常常希望打开错误提示,因此makefile中一般有这一句:override CFLAGS += -Wall

静态规则

意义

  • 将大量的规则使用通配的方法合并为1条规则

做法:

1
2
3
4
5
6
BIN=a b
CC=gcc
all:$(BIN)

$(BIN):%:%.c
$(CC) $^ -o $@

这里将所有无后缀文件:同名.c,也可以C = $(BIN):%=%.c即将所有无后缀文件名改名为同名.c放入C中,当然不是真的改它,只是一个字符串处理而已

内置函数

意义:

  • makefile里面能用的指令不多,字符串内置函数可以精细化处理一些字符需求
1
2
3
4
SRC=$(wildcard *.c)#匹配目录下所有的.c
BIN:=$(patsubst %.c,%,$(SRC))#将.c改成无后缀
BIN:=$(filter-out c,$(BIN))#将c过滤掉
# 最后BIN将会被处理成无后缀文件名,除了c以外

嵌套Makefile

意义:

  • 让make可以文件逐级编译,每层都有自己的Makefile
1
2
3
4
5
6
7
all: $(BIN)
$(MAKE) -C sub_dir/


clean:
rm -f *.o $(BIN)
$(MAKE) -C sub_dir/ clean

LVGL

意义:

  • 为Linux提供比较轻量优美的前端框架