Linux 进程与线程

文章目录

进程与线程

进程

程序的一次执行及其所包含资源的总称。

程序:可执行程序代码
资源:打开文件、挂起信号、地址空间、数据段等

进程构成

正文段

存放进程运行的代码,描述进程需完成的功能

进程数据段

存放正文段在执行期间所需的数据和工作区(包括全局变量、动态分配的空间(调用malloc函数))
用户栈也在该数据段开辟,存放函数调用时栈帧、局部变量

系统堆栈

每个进程捆绑一个,进程在内核态下工作时使用
保存中断现场、执行函数调用时的参数和返回地址等
其中最重要的数据结构是进程控制块(PCB)

进程虚拟地址结构

A

用户空间(0x 0000 0000 ~ 0x bfff ffff)

可执行映象
进程运行时堆栈
进程控制信息,如进程控制块

内核空间(0x c000 0000以上)

内核被映射进程内核空间
只允许进程在核心态下访问

Linux进程描述符

Linux 内核利用数据结构task_struct来描述,它代表一个进程的PCB

内核以task_struct感知进程的存在

当建立新进程的时候,Linux 为新的进程分配一个 task_struct 结构

进程描述符

数据结构:task_struct;
定义位置:include/linux/sched.h
include/linux/sched.h 中的struct task_struct

进程描述符向量结构

数据结构:task[NR_TASKS]
定义位置: include/linux/sched.h
定义格式
struct task_struct *task[NR_TASKS] = {&init_task} //指针数组,方便遍历
#define NR_TASKS 512 //全局变量NR_TASKS记录系统可容纳进程数,默认大小是512

Linux进程描述符的信息组成

进程状态信息(state, flags, ptrace)
调度信息(static_prio, normal_proi, run_list, array, policy)
内存管理(mm, active_mm)
进程状态位信息(binfmt, exit_state, exit_code, exit_signal)
身份信息(pid, tgid, uid, suid, fsuid, gid, egid, sgid, fsgid)
家族信息(real_parent, parent, children, sibling)
进程耗间信息(realtime, utime, stime, starttime)
时钟信息(it_prof_expires, it_virt_expires, it_sched_expires)
文件系统信息(link_count, fs, files)
IPC信息(sysvsem, signal, sighand, blocked, sigmask, pending)

state:进程当前的状态

​ 功能:表征进程的可运行性

​ 状态定义[include/linux/sched.h]

在这里插入图片描述

Linux进程的状态

运行态/就绪态

TASK_RUNNING:正在运行或已处于就绪只等待CPU调度
TASK_TRACED:供调试使用

被挂起状态

TASK_INTERRUPTIBLE:可被信号或中断唤醒进入就绪队列
TASK_UNINTERRUPTIBLE:等待资源,不可被其他进程中断
TASK_STOPPED:被调试暂停,或收到SIGSTOP等信号

不可运行态

TASK_ZOMBIE:正在终止(已释放内存、文件等资源,但内核数据结构信息未释放),等待父进程通过wait4()或waitpid()回收
TASK_DEAD:已退出且不需父进程回收的进程的状态

(调度器主要处理运行/就绪和被挂起两种状态下的进程)

状态图

在这里插入图片描述

sleep_on()

TASK_RUNNING->TASK_UNINTERRUPTIBLE

拥有CPU的进程申请资源无效时,通过sleep_on(),将进程从TASK_RUNNING切换到TASK_UNINTERRUPTIBLE状态。
sleep_on()函数作用就是将current进程的状态置成TASK_UNINTERRUPTIBLE,并加到等待队列中
一般来说引起状态变成TASK_UNINTERRUPTIBLE的资源申请,都是对一些硬件资源的申请,如果得不到这些资源,进程将不能执行下去,不能由signal信号或时钟中断唤醒回到TASK_RUNNING状态。

interruptible_sleep_on()

TASK_RUNNING->TASK_INTERRUPTIBLE

拥有CPU的进程申请资源无效时,通过该函数将进程从TASK_RUNNING切换到TASK_INTERRUPTIBLE状态。
interruptible_sleep_on()函数作用就是将current进程的状态置成TASK_INTERRUPTIBLE,并加到等待队列中。
处于TASK_INTERRUPTIBLE状态的进程在资源有效时被wake_up()、wake_up_interruptible()或wake_up_process()唤醒,或收到signal信号以及时间中断后被唤醒

sleep_on_timeout()

TASK_RUNNING->TASK_UNINTERRUPTIBLE

interruptible_sleep_on_timeout()

TASK_RUNNING->TASK_INTERRUPTIBLE

虽然在申请资源或运行中出现了某种错误,但是系统仍然给进程一次重新运行的机会。调用该函数将进程从TASK_RUNNING切换到TASK_INTERRUTIBLE状态,并等待规定的时间片长度,再重新试一次。

wake_up()
TASK_UNINTERRUPTIBLE-> TASK_RUNNING
  TASK_INTERRUPTIBLE-> TASK_RUNNING

处于TASK_UNINTERRUPTIBLE状态的进程不能由signal信号或时钟中断唤醒,只能由wake_up()wake_up_process()唤醒
wake_up()函数的作用是将wait_queue中所有状态为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE的进程
状态置为TASK_RUNNING,并将它们都
放running队列
中去,即唤醒所有等待在该队列上的进程。

进程标识

成员名:pid_t pid
功能

内核通过pid标识每个进程
pid与进程描述符之间有严格的一一对应关系

数据类型说明

int类型

取值范围

0 ~ 32767

最大值修改

/proc/sys/kernel/pid_max

生成新pid

get_pid()函数。+1;循环

获取进程pid

ps命令

ps -ef | more		 //查看所有进程
ps -aux | grep java  //查看某一进程(例子)

加入-aux 显示所有状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7LBYlZM0-1682169432780)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20230421202358597.png)]

访问/proc/pid
getpid()<-sys_getpid()

终止进程
kill -9 [PID]

-9 表示强迫进程立即停止

通常用 ps 查看进程 PID ,用 kill 命令终止进程

kill -SIGUSR1 [PID]

用于向进程发送 SIGUSR1 信号(用户自定义信号1),其中 [PID] 是要发送信号的进程的进程ID。

/proc目录下的进程信息
ls /proc/某pid号

status :进程状态信息
ctl :进程控制文件
psinfo:进程ps 信息
as:进程地址空间
map:进程映射信息
object:进程对象信息
sigact:进程信号量操作
sysent:进程系统调用信息
lwp/tid:进程核心线程标识符目录
lwp/tid/lwpstatus:核心线程状态
lwp/tid/lwpctl:核心线程控制文件
lwp/tid/lwpsinfo:核心线程ps 信息

进程组标识

成员名

pid_t tgid

功能

标识进程是否属于同组,组ID是第一个组内线程(父进程)的ID
线程组中的所有线程共享相同的PID

用户相关的进程标识信息

功能

控制用户对系统资源的访问权限

分类
用户标识uid及组标识gid

​ 通常是进程创建者的uid和gid

有效用户标识euid及有效组标识egid

​ 有时系统会赋予一般用户暂时拥有root的uid和gid(作为用户进程的euid和egid),以便于进行运作

备份用户标识suid及备份组标识sgid

​ 在使用系统调用改变uid和gid时,利用其保留真正uid和gid

文件系统标识fsuid及文件系统组标识fsgid

​ 检查对文件系统访问权限时使用,通常与euid及egid相等
​ 在NSF文件系统中,NSF服务器需要作为一个特殊的进程访问文件,此时只修改客户进程的fsuid和fsgid,而不改变euid 及egid,可避免受恶意攻击

获取用户相关的进程标识信息的代码示例
#include <sys/types.h>
#include<unistd.h>
#include <stdio.h>
#include<pwd.h>

int main(int argc,char **argv)
{
    pid_t my_pid,parent_pid;
    uid_t my_uid,my_euid;
    gid_t my_gid,my_egid;
    struct passwd *my_info;
    my_pid=getpid();
    parent_pid=getppid();
    my_uid=getuid();
    my_euid=geteuid();
    my_gid=getgid();
    my_egid=getegid();
    my_info=getpwuid(my_uid);
    printf("Process ID: %ld\n",my_pid);
    printf("Parent ID: %ld\n",parent_pid);
    printf("User ID: %ld\n",my_uid);
    printf("Effective User ID: %ld\n",my_euid);
    printf("Group ID: %ld\n",my_gid);
    printf("Effective Group ID: %ld\n",my_egid);
}

Linux 2.6进程家族信息的访问

获取父进程
struct task_struct *my_parent = current->parent
获取所有子进程

(实质是遍历list_head结构)

struct task_struct *task;
struct list_head *list;

list_for_each(list, &current->children)
{
	task = list_entry(list, struct task_struct, sibling);	//task指向当前的某个子进程
}

等待队列

TASK_STOPPED和TASK_ZOMBIE状态的进程不在专门的链表中,它们的父进程可以通过进程的PID或进程间亲属关系检索到子进程。

TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程可以再分成很多类,每一类对应一个特定的事件。在这种情况下,进程状态提供的信息满足不了快速检索,因此,内核引进了另外的进程链表,叫做等待队列(请查看状态图)

Linux中的进程控制

Linux进程的创建
Linux进程的执行
Linux进程的等待
Linux进程的终止

除了系统启动之后的第一个进程由系统创建之外其余所有的进程都必须由已存在的进程来创建,主要工作为:创建子进程的PCB,连接其代码,处理父子进程间的关系

系统启动过程

在这里插入图片描述

0号进程

所有进程的祖先,在OS启动时创建。由它执行cpu_idle()函数,当没有其他进程处于TASK_RUNNING的时候,调度程序会选择0号进程运行

维护Linux内核代码段、数据段及堆栈

唯一一个通过静态分配创建的进程[arch/x86/kernel/init_task.c]

​ INIT_MM, INIT_FS, INIT_FILES, INIT_SIGNALS, INIT_SIGHAND等宏初始化进程描述符的各个对应域
​ INIT_TASK完成init_task变量中进程描述符初始化
​ INIT_THREAD_INFO完成对init_thread_info中thread_info及内核堆栈的初始化
​ 主内核页全局目录存放在swapper_pg_dir变量中

start_kernel函数(/init/main.c)初始化内核所需所有数据结构、激活中断、创建1号内核线程

1号进程

号进程创建1号进程,1号进程通常称为init进程。它首先创建一些后台进程来维护系统,然后进行系统配置,执行shell编写的初始化程序。然后**转入用户态运行

由0号进程在start_kernel()函数中调用rest_init创建
在这里插入图片描述

调度程序选择到init进程时,init进程开始执行init()函数

init()说明:

实现从内核态到用户态的切换
为常规内核任务初始化一些必要的内核线程
kflushd:刷新‘脏’缓冲区中内容到磁盘,归还内存
kswapd:执行内存回收功能
调用系统调用execve()装入可执行程序init
init内核线程变成一个普通进程(拥有自己的进程内核数据结构)
init进程从不终止,因为它创建和监控操作系统外层的所有进程的活动

写时复制技术(Copy-On-Writing,COW)

允许父子进程能读相同的物理页

简单过程描述

先通过复制页表项共享这个页面,将父、子进程可写虚拟内存页的页表项均标志为只读
当父或子进程向该内存页写入数据时,就会引起一次页面异常(缺页中断),页面异常处理程序将重新分配一个物理页面,并完成真正的内容复制
fork()调用:可理解为逻辑拷贝整个进程的地址空间,仅当试图修改页面才真正的拷贝

Linux的进程创建

sys_fork()/sys_clone()/sys_vfork()其中,sys_clone()创建轻量级进程,必须指定要共享的资源
exec()系统调用:执行一个新程序
exit()系统调用:终止进程(进程也可以因收到信号而终止)
在这里插入图片描述

轻量级进程

允许父子进程共享页表、打开文件列表、信号处理等数据结构
但每个进程应该有自己的程序计数器、寄存器集合、核心栈和用户栈

几种创建方式
在终端输入命令,由shell进程创建一个新进程
进程创建函数

pid_t fork(void);
pid_t vfork(void);
int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);

创建轻量级线程

三函数都调用同一内核函数do_fork( ) [/kernel/fork.c]

函数调用形式
do_fork(unsigned long clone_flag, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int _user *parent_tidptr, int _user *child_tidptr);
参数说明
clone_flag:子进程创建相关标志
stact_start:将用户态堆栈指针赋给子进程的esp
regs:指向通用寄存器值的指针
stack_size:未使用(总设为0)
parent_tidptr:父进程的用户态变量地址,若需父进程与新轻量级进程有相同PID,则需设置CLONE_PARENT_SETTID
child_tidptr:新轻量级进程的用户态变量地址,若需让新进程具有同类进程的PID ,需设置CLONE_CHILD_SETTID
进程创建CLONE参数标志说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mggjOc8b-1682169432783)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20230421212812682.png)]

fork()函数

说明
子进程完全复制父进程的资源
子进程的执行独立于父进程
进程间数据共享需通过专门的通信机制来实现
返回值
父进程执行fork()返回子进程的PID值
子进程执行fork()返回0
调用失败返回-1

该函数被调用一次,会返回两次。给子进程的返回值是0,给父进程的返回值是子进程的进程ID,然后子进程和父进程继续执行fork之后的指令

当fork()返回到用户空间时,向子进程内核栈压入返回值0,而将子进程的pid作为返回值压入到父进程内核堆栈

fork()调用代码结构示例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    pid_t val;

    printf("PID before fork():%d\n", (int)getpid());
    if(val = fork())
        printf("parent process fork():%d\n", (int)getpid());
    else
        printf("child process fork():%d\n", (int)getpid());
}
父子进程执行线索

在这里插入图片描述

vfork()函数

新进程可共享父进程的内存地址空间
在这里插入图片描述

说明

子进程作为父进程的一个单独线程在其地址空间运行
子进程从父进程继承控制终端、信号标志位、可访问的主存区、环境变量和其他资源分配
子进程对虚拟空间任何数据的修改都可为父进程所见
进程将被阻塞,直到子进程调用execve()或exit()

注意父进程将被阻塞!!!

与fork()的关系

功能相同,但vfork()不拷贝父进程的页表项
子进程只执行exec()时,vfork()为首选

进程调用exec()函数时,该进程完全由新程序替代,新程序从main开始执行
exec()并不创建新进程,前后进程ID不变,但用另外一个程序替代当前进程的正文、数据、堆栈等

在这里插入图片描述

vfork()系统调用示例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>


int main()
{
   int data = 0;
   pid_t pid;
   int choose = 0;
   while((choose = getchar()) != 'q')
   {
       switch(choose)
       {
           case '1':
               pid = fork();
               if(pid < 0)
                   printf("error\n");
               if(pid == 0)
               {
                   data++;
                   exit(0);					//程序会立即退出运行,并向操作系统返回退出码0
               }   
               wait(pid);
               if(pid > 0)
               {
                   printf("data is %d\n",data);
               }
               break;
           case '2':  
               pid = vfork();				//vfork
               if(pid < 0)
                   printf("error\n");
               if(pid == 0)
               {
                   data++;
                   exit(0);
               }   
               wait(pid);
               if(pid > 0)
               {
                   printf("data is %d\n",data);
               }
               break;
            default :
               break;
       }
   }
}

进程等待

创建子进程后,父进程的选择有以下几种:
(1) 父进程不理睬子进程,继续执行。如果子进程先于父进程消亡的话,则内核会发送一个信号,通知父进程。
(2)父进程暂停,睡眠,等待子进程结束后继续运行
(3)父进程自行结束,使用exit()。

wait()系统调用

父进程调用wait()类系统调用检查子进程是否终止

父进程调用wait()后进入阻塞队列中。当子进程结束时会产生一个终止状态字,系统内核再向父进程发出SIGCHILD信号。当接收到信号时,父进程提取子进程的终止状态字,从wait()返回到原程序。

Wait()系统调用的作用
获取子进程终止的消息
清除子进程的所有独占资源

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

说明
均通过wait4()系统调用实现
进程终止时,会向父进程发送SIGCHLD信号

调用wait( )和waitpid( )的进程的可能状态
阻塞
如果子进程还在运行
正常返回
返回子进程的终止状态(其中一个子进程终止)
出错返回
没有子进程

进程撤消

撤销时机

​ 主动撤销:执行完代码,通知内核释放进程的资源
​ 被动撤消:内核强迫杀死进程
​ 进程接收到一个不能处理或忽视的信号
​ 在内核态产生一个不可恢复的CPU异常,而内核此时正代表该进程在运行

撤消相关进程状态

​ 进程已死,但必须保存它的描述符,在得到通知后才可以删除
僵死状态,表明进程已死,但需要等待父进程删除(如通过wait())

撤消过程
进程终止

​ 释放进程占有的大部分资源
​ 进程终止的一般方式是exit()或_exit()系统调用
​ 该系统调用可由编程者明确地在代码中插入
​ 控制流到达主过程最后一条语句时,自动执行exit()
​ 信号机制

进程删除

彻底删除进程的所有数据结构
​ 父进程调用wait()类系统调用获知子进程合法终止后,即可删除该进程

终止函数

exit(int status)
_exit(int status)

status说明
正常结束时返回0,否则表示出错信息

两函数区别
_exit()直接使进程停止工作,做清除和销毁工作
exit()在调用do_exit()系统调用前检查文件打开情况,清理I/O缓冲

进程的终止分2步:
自己调用函数exit()将自己的状态变为僵死状态,而后向父进程发送终止信号。
由父进程调用wait()函数,回收僵死状态的子进程,将其从内存中彻底清除。

内核可以强迫进程终止
当进程接收到一个不能处理或忽视的信号时
当在内核态产生一个不可恢复的CPU异常而内核此时正代表该进程在运行

若子进程包含终止代号,则父进程通过release()释放僵死进程的描述符
释放进程id
从进程链表中删除进程描述符
释放存放进程描述符的内存区

进程切换(process switching)

为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换(或任务切换,上下文切换)

进程上下文

包含了进程执行需要的所有信息
用户地址空间:包括程序代码,数据,用户堆栈等
控制信息:进程描述符,内核堆栈等
硬件上下文

线程

进程中活动的对象。

有独立的程序计数器、进程栈及一组进程寄存器
在这里插入图片描述

开发线程的目的

为了进一步减少处理器的空转时间、支持多处理器以及减少系统的开销

它是进程内独立的运行线路,是内核调度的最小单元,也可以称为轻量级进程

基本概念

应用程序并发执行多种任务的一种机制

一个进程中可以创建多个线程,多个线程共享进程的地址空间

创建线程无须对地址空间进行复制

共享

共享进程的内存段,包括数据段、堆区

非共享

进程的区对线程不共享的,每个线程都拥有属于自己的栈区,用于存放函数的参数值、局部变量的值、返回地址等。

在这里插入图片描述

线程的创建

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

第一个参数:为函数参数thread表示新创建的线程的标识符,或者称为线程的ID
第二个参数:用来设置线程属性参数attr指向一个pthread_attr_t类型的结构体,用以指定新创建的线程的属性(如线程栈的位置和大小、线程调度策略和优先级以及线程的状态),如果attr被设置为NULL,则线程将采用默认的属性。
第三个参数:是线程运行函数的起始地址参数start_routine为函数指针,因此该参数只需传入函数名即可。
最后一个参数运行函数的参数
如果线程运行函数不需要参数,最后一个参数设为空指针。第二个参数也设为空指针,这样将生成默认属性的线程

例子
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

void *thread_one( void * arg)
{
   while(1)
   {
   printf("this is thead one\n");
   sleep(2);
   }
}
int main( int arg, char ** argv)
{
   pthread_t thread;
   pthread_create( &thread, NULL, thread_one, NULL);
   while(1)
   {
   sleep(2);
   printf("primary thread exit!\n");
   }
   return 0; 
}

pthread_t本质上是一个经强制转化的无符号长整型的指针

获取线程ID
#include <pthread.h>
pthread_t pthread_self(void);
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
void *thread_one( void * arg)
{
  printf("this is thead one, pid=%ld\n",pthread_self());
}
int main( int arg, char ** argv)
{
   pthread_t thread;

   pthread_create( &thread, NULL, thread_one, NULL);

   sleep(1);
	printf("this primary thread , pid=%ld\n", pthread_self());

   return 0; 
}

线程终止与回收

几种情况都会导致线程的退出

(1)线程的执行函数执行return语句并返回指定值
(2)线程调用pthread_exit()函数。
(3)调用
pthread_cancel()函数取消线程。
(4)任意线程调用
exit()函数,或者main()函数中执行了return
语句,都会造成进程中的所有线程立即终止。

pthread_exit()函数将终止调用线程,且参数可被其他线程调用

pthread_join()函数来获取。参数retval指定了线程的返回值。如果一个线程调用了pthread_exit()函数,但其他线程仍然继续执行。

#include <pthread.h>
void pthread_exit(void *retval);
pthread_cancel函数
int pthread_cancel(pthread_t thread)

发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。它只提出请求。线程在取消请求(pthread_cancel)发出后会继续运行,直到到达某个取消点(CancellationPoint)。取消点是线程检查是否被取消并按照请求进行动作的一个位置.

int pthread_setcancelstate(int state,   int *oldstate)  

设置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为NULL则存入原来的Cancel状态以便恢复。

int pthread_setcanceltype(int type, int *oldtype)  

设置本线程取消动作的执行时机,type由两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出立即执行取消动作(退出);oldtype如果不为NULL则存入运来的取消动作类型值。

取消点例子:

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

 void* thr(void* arg)
{

         pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);
         pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);	//收到信号继续执行
     
         while(1)
         {
             
             pthread_testcancel();
     		/*某些情况下,希望通过pthread_cancel结束某个线程,但是被结束的线程必须在某			   一个点来进行退出操作,就需要用到pthread_testcancel。*/
         }
         printf("thread is not running\n");
         sleep(2);

}

int main()
{
         pthread_t tid;
         int err;
         err = pthread_create(&tid,NULL,thr,NULL);	
         pthread_cancel(tid);			//注释掉就不会取消
         pthread_join(tid,NULL);
         sleep(1);
         printf("Main thread exited\n");
         return 0;

}
pthread_exit函数

一个线程的结束有两种途径,一种是象我们上面的例子,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit实现 。
函数原型:

void  pthread_exit (void *__retval)

唯一的参数是函数的返回代码 。如果pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给 thread_return
需要注意的是:一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。

pthread_join()函数

用于等待指定thread标识的线程终止。如果线程终止,则pthread_join()会立即返回。参数retval如果为非空指针,那么此时参数将会保存标识符为参数thread的线程退出时的返回值,即pthread_exit()中指定的参数。

若线程并未进行分离,则必须使用pthread_join()来进行回收资源。如果未能进行,那么线程终止时将产生与僵尸进程类似的僵尸线程。如果僵尸线程积累过多,不仅浪费资源,而且可能无法继续创建新的线程。

什么是线程分离:当线程被设置为分离状态后,线程结束时,它的资源会被系统自动的回收

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

用来等待一个线程的结束
第一个参数为被等待的线程标识符 。
第二个参数为一个用户定义的指针,用来存储被等待线程返回值
这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回

例子
pthread_join ( mythread, NULL );

void * thread_result;
res = pthread_join(myThread, &thread_result);

void  pthread_exit (void *__retval)
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define NUM_THREADS 4

void *BusyWork(void *t) /* 线程函数 */
{
    int i;
    long tid;
    double result=0.0;
    tid = (long)t;
    
    printf("Thread %ld starting...\n",tid);
    for (i=0; i<1000000; i++)
    {
    	result = result + sin(i) * tan(i); /* 进行数学运算 */
    }
    printf("Thread %ld done. Result = %e\n",tid, result);
    pthread_exit((void*) t); /* 带计算结果退出 */
}

int main (int argc, char *argv[])
{
    pthread_t thread[NUM_THREADS];
    int rc;
    long t;
    void *status;
    
     for(t=0; t<NUM_THREADS; t++)
     {
        printf("Main: creating thread %ld\n", t);
        rc = pthread_create(&thread[t], NULL, BusyWork, (void *)t); /* 创建线程 */
        if (rc) 
        {
        printf("ERROR; return code from pthread_create() is %d\n", rc);
        exit(-1);
     	} 
     }
    for(t=0; t<NUM_THREADS; t++) 
    {
        rc = pthread_join(thread[t], &status); /*等待线程终止,并获取返回值*/
        if (rc) 
        {
        printf("ERROR; return code from pthread_join() is %d\n", rc);
        exit(-1);
    	}
   	 	printf("Main: completed join with thread %ld having a status of %ld\n",t,				(long)status);
    }
    printf("Main: program completed. Exiting.\n");
    pthread_exit(NULL);
}

线程的分离

pthread_detach设置线程分离
默认的情况下,线程是可连接的(也可称为结合态)。通俗地说,就是当线程退出时,其他线程可以通过调用pthread_join()函数获取其返回状态。但有时,在编程过程中,程序并不关心线程的返回状态,只是希望系统在线程终止时能够自动清理并移除。在这种情况下,可以调用pthread_detach()函数并向thread参数传入指定线程的标识符,将该线程标记为处于分离状态(分离态)

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

一旦线程处于分离状态,就不能再使用pthread_join()函数来获取其状态,也无法使其重返“可连接”状态。

pthread_attr_setdetachstate实现线程分离

在线程刚一创建时即进行分离(而非之后再调用pthread_detach()函数)。首先可以采用默认的方式对线程属性结构进行初始化,接着为创建分离线程而设置属性,最后再以此线程属性结构来创建新线程,线程一旦创建,就无须再保留该属性对象。最后将其摧毁

#include <pthread.h>
	
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

设置线程分离状态的函数为pthread_attr_setdetachstate()。

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

detachstate:用来设置线程的状态,设置PTHREAD_CREATE_DETACHED(分离态)与PTHREAD_CREATE_JOINABLE(结合态)。

pthread_attr_t:

typedef struct
{
       int                       detachstate;   // 线程的分离状态
       int                       schedpolicy;   // 线程调度策略
       structsched_param         schedparam;    // 线程的调度参数
       int                       inheritsched;  // 线程的继承性
       int                       scope;         // 线程的作用域
       size_t                    guardsize;     // 线程栈末尾的警戒缓冲区大小
       int                       stackaddr_set; // 线程的栈设置
       void*                     stackaddr;     // 线程栈的位置
       size_t                    stacksize;     // 线程栈的大小
} pthread_attr_t;

线程的取消

在通常情况下,程序中的多个线程会并发执行,每个线程处理各自的任务,直到其调用pthread_exit()函数或从线程启动函数中返回。但有时候也会用到线程的取消,即向一个线程发送一个请求,要求其立即退出。例如,一组线程正在执行一个任务,如果某个线程检测到错误发生,需要其他线程退出,此时就需要取消线程的功能。

设置线程取消状态

pthread_cancel()函数向由thread指定的线程发送一个取消请求。发送取消请求后,函数pthread_cancel()立即返回,不会等待目标线程的退出

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

目标线程会发生的结果及发生的时间取决于线程的取消状态和类型

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);

pthread_setcancelstate()函数会将调用线程的取消状态设置为参数state所给定的值。参数state可被设置为PTHREAD_CANCEL_DISABLE(线程不可取消),如果此类线程收到取消请求,则会将请求挂起,直至将线程的取消状态置为启用。也可被设置为PTHREAD_CANCEL_ENABLE(线程可以被取消),一般,新创建的线程默认为可以取消。参数oldstate用以保存前一次状态。

设置线程取消类型

如果需要设置线程为可取消状态,则可以选择取消的类型

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);

pthread_setcanceltype()函数用以设置当前线程的可取消的类型,上一次的取消类型保存在参数oldtype中。参数type可以被设置为PTHREAD_CANCEL_DEFERRED,表示线程接收取消操作后,直到运行到“可取消点”后取消。type也可以被设置为PTHREAD_CANCEL_ASYNCHRONOUS,表示接收到取消操作后,立即取消

如果不对线程取消类型进行设置,则线程默认的设置为PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DEFERRED,即线程可被取消,并且在“取消点”后取消

pthread_testcancel()函数用来给当前线程设置一个“可取消点”

线程的属性

pthread_create()之前的属性设置

用pthread_create函数创建一个线程,该函数的第二个参数属性结构为pthread_attr_t,它同样在头文件pthread.h中定义,属性值不能直接设置,须使用相关函数进行操作,初始化的函数为 pthread_attr_init,这个函数pthread_creat函数之前调用。

属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。

线程分离状态

​ 线程的分离状态决定一个线程以什么样的方式来终止自己非分离的线程终止时,其线程 ID和退出状态将保留直到另外一个线程调用pthread_join分离的线程在当它终止时,所 有的资源将释放,我们不能等待它终止。

设置线程分离状态的函数为
pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)
第二个参数可选为
PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE (非分离线程)。

要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在 pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号

要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timewait函数,让这个线程等待一会儿,留出足够时间让pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。

一旦分离就不能再恢复!!!

绑定

关于线程的绑定,牵涉到另外一个概念:轻量进程(LWP:Light Weight Process)。轻量进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻量进程来实现的一个轻量进程可以控制一个或多个线程

默认状况下,启动多少轻量进程、哪些轻量进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻量进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻量进程的,绑定的线程可以保证在需要的时候它总有一个轻量进程可用。通过设置被绑定的轻量进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

设置线程绑定状态的函数为 pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)PTHREAD_SCOPE_PROCESS(非绑定的)。

#include <pthread.h>
    pthread_attr_t attr;
    pthread_t tid;
    /*初始化属性值,均设为默认值*/
    pthread_attr_init(&attr); 
    pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
    pthread_create(&tid, &attr, (void *) my_function, NULL); 
typedef struct
{
       int                      detachstate;   // 线程的分离状态
       int                      schedpolicy;   // 线程调度策略
       structsched_param    schedparam;    // 线程的调度参数
       int                      inheritsched;  // 线程的继承性
       int                      scope;         // 线程的作用域
       size_t                ardsize;  // 线程栈末尾的警戒缓冲区大小
       int        	           stackaddr_set; // 线程的栈设置
       void*                 stackaddr;     // 线程栈的位置
       size_t                stacksize;     // 线程栈的大小
} pthread_attr_t;

sched_policy,表示新线程的调度策略,主要包括SCHED_OTHER(正常、非实时)(不允许设置优先级)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过pthread_attr_setschedpolicy ()来改变。在设置线程属性 pthread_attr_t 之前,通常先调用pthread_attr_init来初始化,之后来调用相应的属性设置函数

实时线程

单位时间相应能力强,里面拥有1-99个静态优先级,数字越大,优先级越高(所谓的优先级指的经过特殊的处理,我们可以让某个人物能够在系统中被更优先的响应,从而分出的从高到低的级别),需要有管理员权限才能启动实时线程

特点:
实时线程分99个静态优先级,数字越大,优先级越高
高优先级的实时线程会完全抢占低优先级实时线程的资源(指令运行资源)
在实时线程当中支持抢占调度策略跟轮询调度策略
拥有抢占所有实时线程运行资源的能力
必须拥有超级用户权限才能够运行

非实时线程

单位时间中,并没有过分的去在乎响应能力的一个线程,里面只有一个静态优先级0,也就是在非实时线程中,它是没有静态优先级的概念的,他的所有的执行过程都是由系统自动分配的

特点:
非实时线程只有一个静态优先级,所以同时非实时线程的任务无法抢占他人的资源
在非实时线程当中只支持其他调度策略(自动适配的系统分配的调度策略)
不拥有抢占所有运行资源的能力
支持动态优先级系统自适应,从-20到19的动态优先级(nice值)

线程中支持三种调度策略

抢占式调度策略,在同一静态优先级的情况下,抢占调度策略的线程一旦运行到便会一直抢占CPU资源,而其他同一优先级的只能一直等到这个抢占式调度策略的线程退出才能被运行到(非实时线程会有一小部分资源分配到)
轮询式调度策略,在同一静态优先级的情况下,大家一起合理瓜分时间片,不会一直抢占CPU资源(非实时线程会有一小部分资源分配到)
其他普通式调度策略,只能作用于非实时线程,由系统自动分配时间片,并且根据运行状态自动分配动态优先级

注意点:
抢占式调度策略跟轮询式调度策略只能在实时线程中被设置,也就是静态优先级1-99的区域内设置,普通非实时线程不能设置

设置线程的调度策略

#include <pthread.h> 
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);

函数功能:设置线程的调度策略属性
attr:线程属性结构体地址 policy:调度策略

**SCHED_FIFO:**抢占式调度,同一优先级中,一旦运行到设置了这个参数的线程CPU将会一直被该线程所占领,不会分配资源给其他实时线程,会分配一点资源给非实时线程
SCHED_RR:轮询式调度,同一优先级总,遇到这个设置的线程,将会给其运行一段时间后,又继续给下一个人运行(相当于大家平均运行),会分配一点资源给非实时线程上面的两种是针对静态优先级1-99的实时线程才能设置的。
SCHED_OTHER:其他普通的调度策略,仅能设置与0静态优先级,也就是非实时线程,让这条线程成为一个由
系统去自动根据动态优先级分配资源
的任务。

线程默认调度策略是 SCHED_OTHER

返回值:成功的情况下,返回0,失败返回非0值,errno不会被设置

pthread_attr_setinheritsched //设置线程是否继承父线程调度策略

#include <pthread.h> 
int pthread_attr_setinheritsched(pthread_attr_t *attr,int inheritsched);

函数功能:设置线程是否继承父线程调度策略

参数:
attr:线程属性结构体地址
inheritsched:是否继承父线程的调度策略
PTHREAD_EXPLICIT_SCHED:不继承,只有不继承父线程的调度策略才可以设置线程的调度策略
PTHREAD_INHERIT_SCHED:继承父进程的调度策略

返回值:成功的情况下,返回值为0,失败返回非0值,errno不会被设置

优先级(实时才有效)

放在结构sched_param中,目前仅有一个sched_priority 整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。用函pthread_attr_getschedparam和函数 pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去

pthread_attr_setschedparam

函数功能:设置静态优先级
参数:
attr:线程属性结构体地址
param:优先级结构体,里面只有元素sched_priority,用来登记线程的静态优先级的值。

struct sched_param 
{     
    int sched_priority;     /* Scheduling priority */ 
}

返回值:成功的情况下,返回值为0,失败返回非0值,errno不会被设置

获取静态优先级的最小值与最大值的函数
获取最小值:sched_get_priority_min(SCHED_FIFO);
获取最大值:sched_get_priority_max(SCHED_FIFO);

#include <pthread.h>
#include <sched.h>

    pthread_attr_t attr;
	pthread_t tid;
	sched_param param;
    int newprio=20; 
    /*初始化属性*/
    pthread_attr_init(&attr); 				//线程属性初始化
    /*设置优先级*/
    pthread_attr_getschedparam(&attr, &param); //先取优先级
    param.sched_priority=newprio;			  //值修改
    pthread_attr_setschedparam(&attr, &param); //存放回去
    pthread_create(&tid, &attr, (void *)myfunction, myarg); //创建线程

**改变优先级还需要系统超级用户权限!!!**一般情况下很少会设置优先级

调度策略

SCHED_FIFO:先进先出调度

1)如果⼀个SCHED_FIFO线程被⾼优先级线程抢占了,那么它将会被添加到该优先级等待列表的⾸部,以便当所有⾼优先级的线程阻塞的时候得到继续运⾏;
2)当⼀个阻塞的SCHED_FIFO线程变为可运⾏时,它将被加⼊到同优先级列表的尾部
3)如果通过系统调⽤改变线程的优先级,则根据不同情况有不同的处理⽅式:
a)如果优先级提⾼了,那么线程会被添加到对应新优先级的尾部,因此,这个线程有 可能会抢占当前运⾏的同优先级的线程;
b)如果优先级没变,那么线程在列表中的位置不变
c)如果优先级降低了,那么它将被加⼊到新优先级列表的⾸部
根据POSIX.1-2008规定,除了使⽤pthread_setschedprio(3)以外,通过使⽤其他⽅式改变 策略或者优先级会使得线程加⼊到对应优先级列表的尾部;
4)如果线程调⽤了sched_yield(2),那么它将被加⼊到列表的尾部;
SCHED_FIFO会⼀直运⾏,直到它被IO请求阻塞,或者被更⾼优先级的线程抢占,亦或者调⽤了sched_yield()

SCHED_RR:轮转调度

SCHED_RR是SCHED_FIFO的简单增强,除了对于线程占⽤的时间总量之外,对于SCHED_FIFO适⽤的规则对于SCHED_RR同样适⽤;但每个线程只允许在最大时间范围内运行,如果SCHED_RR线程的运⾏时间⼤于等于时间总量,那么它将被加⼊到对应优先级列表的尾部;如果SCHED_RR线程被抢占了,当它继续运⾏时它只运⾏剩余的时间量;时间总量可以通过sched_rr_get_interval()函数获取;

SCHED_OTHER:默认Linux时间共享调度

SCHED_OTHER只能⽤于优先级为0的线程,SCHED_OTHER策略是所有不需要实时调度线程的统⼀标准策略;调度器通过动态优先级来决定调⽤哪个SCHED_OTHER线程,动态优先级是基于nice值的,nice值随着等待运⾏但是未被调度执⾏的时间总量的增长⽽增加;这样的机制保证了所有SCHED_OTHER线程调度的公平性;

线程同步互斥机制

互斥锁

线程在访问共享资源的过程中被其他线程打断,其他线程也开始访问共享资源导致了数据的不确定性。对于上述情况而言,最好的解决办法是当一个线程在进行共享资源的访问时其他线程不能访问,保证对于共享资源操作的完整性。

通常把对共享资源操作的代码段,称之为临界区,其共享资源也可以称为临界资源

互斥锁的工作原理就是对临界区进行加锁,保证处于临界区的线程不被其他线程打断,确保其临界区运行完整。

同等条件下,对互斥锁的持有是不确定的,先持有锁的线程先访问,其他线程只能阻塞等待。也就是说,互斥锁并不能保证线程的执行先后,但却可以保证对共享资源操作的完整性

在这里插入图片描述

在大多数程序中,线程对互斥锁的持有时间应尽可能短,以避免其他线程等待时间太久,保证其他线程可以尽快获得互斥锁。如果某一线程使用pthread_mutex_trylock()函数周期性的轮询是否可以占有互斥锁,则增加了系统消耗。

死锁

即互斥锁无法解除同时也无法加持,导致程序可能会无限阻塞的情况

1)在互斥锁默认属性的情况下,在同一个线程中不允许对同一互斥锁连续进行加锁操作,因为之前锁处于未解除状态,如果再次对同一个互斥锁进行加锁,那么必然会导致程序无限阻塞等待。

2)多个线程对多个互斥锁交叉使用,每一个线程都试图对其他线程所持有的互斥锁进行加锁。如图所示的情况,线程分别持有了对方需要的锁资源,并相互影响,可能会导致程序无限阻塞,就会造成死锁。

在这里插入图片描述

加锁与解锁的顺序一定是相反的,否则也会导致错误

3)一个持有互斥锁的线程被其他线程取消,其他线程将无法获得该锁,则会造成死锁。

当线程遭取消时,会沿该栈自顶向下依次执行清理函数。当执行完所有的清理函数后,线程终止。pthread_cleanup_push()函数和pthread_cleanup_pop()函数分别负责向调用线程的清理函数栈添加移除清理函数

#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

执行pthread_cleanup_push()函数会将参数routine所含的函数地址添加到调用线程的清理函数栈顶。arg作为调用函数的参数,传递给routine。pthread_cleanup_pop()函数与pthread_cleanup_push()函数必须成对出现在同一函数中。

当线程执行以下操作时,会自动调用清理函数,分别是线程调用pthread_exit()函数、线程被pthread_cancel()函数取消、线程调用pthread_cleanup_pop()函数且参数execute为非零。而本节所讨论的正是线程被取消的情况,而将清除函数的功能设置为解除互斥锁,从而避免一旦线程在持有互斥锁时,被意外取消之后,会自动调用清除函数将互斥锁解除,这样其他线程就不会陷入无限期等待状态。

互斥锁属性
#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

pthread_mutexattr_init()函数为初始化互斥锁属性,一般采用默认的方式传参。pthread_mutexattr_destroy()函数摧毁互斥锁属性。

#include <pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
              restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
              int pshared);

pthread_mutexattr_getpshared()函数和pthread_mutexattr_setpshared()函数的功能分别为获得互斥锁的属性设置互斥锁的属性。参数attr表示互斥锁的属性,参数pshared可以设置为两种情况:(1)PTHREAD_PROCESS_PRIVATE,表示互斥锁只能在一个进程内部的两个线程进行互斥(默认情况);(2)PTHREAD_PROCESS_SHARED,互斥锁可用于两个不同进程中的线程进行互斥,使用时需要在共享内存(后续介绍)中分配互斥锁,再为互斥锁指定该属性即可。

互斥锁类型
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int 	*restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

参数type:

PTHREAD_MUTEX_NORMAL:标准互斥锁,该类型的互斥锁不具备死锁检测功能

PTHREAD_MUTEX_ERRORCHECK:检错互斥锁,对此互斥锁的所有操作都会执行错误检查,这种互斥锁运行起来较一般类型慢,不过却可以作为调试,以发现后续程序在哪里违反了互斥锁的使用规则。

PTHREAD_MUTEX_RECURSIVE:递归互斥锁,该互斥锁维护有一个锁计数器,线程上锁则会将锁计数器的值加1,解锁则会将锁计数器的值减1。只有当锁计数器值降至0时,才会释放该互斥锁。这一类互斥锁与普通互斥锁的区别在于,同一个线程可以多次获得同一个递归锁,不会产生死锁。而如果一个线程多次获得同一个普通锁,则会产生死锁。Linux下的互斥锁默认属性为非递归的。

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

pthread_t tid[2];
int flag1=10;
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化互斥量 */

void * t1(void *arg)
{
    pthread_mutex_lock(&mutexA); /* 线程 1 获取 mutexA */
    printf("t1 get mutexA\r\n");
    flag1++;
    printf("flag1 is %d\r\n",flag1);
    usleep(2000);
    pthread_mutex_unlock(&mutexA); /* 线程 1 释放 mutexA */
    printf("t1 release mutexA\r\n");
    return NULL;
}

void * t2(void *arg)
{
    pthread_mutex_lock(&mutexA);
    printf("t2 get mutexA\n");
    flag1--;
    printf("flag1 is %d\r\n",flag1);
    pthread_mutex_unlock(&mutexA);
    printf("t2 release mutexA\n");

    return NULL;
}

int main(void)
{
    int err;
    
    /* 创建线程 1 */
    err = pthread_create(&(tid[0]), NULL, &t1, NULL );
    if (err != 0) 
    printf("Can't create thread :[%s]", strerror(err));
     
    /* 创建线程 2 */
    err = pthread_create(&(tid[1]), NULL, &t2, NULL);
    if (err != 0)
    printf("Can't create thread :[%s]", strerror(err));
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    return 0;
}
信号量

信号量本身代表一种资源,其本质是一个非负的整数计数器,被用来控制对公共资源的访问。

在访问共享资源之前,都需要先操作信号量的值。操作信号量的值又可以称为PV操作,P操作为申请信号量,V操作为释放信号量。

申请信号量成功时,信号量的值减1,而释放信号量成功时,信号量的值加1。但是当信号量的值为0时,申请信号量时将会阻塞,其值不能减为负数。利用这一特性,即可实现对共享资源访问的控制。

若用于实现互斥时,多线程只需设置一个信号量。若用于实现同步时,则需要设置多个信号量,并通过设置不同的信号量的初始值来实现线程的执行顺序。

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

sem_init()函数被用来进行信号量的初始化。参数sem表示信号量的标识符。pshared参数用来设置信号量的使用环境,其值为0,表示信号量用于同一个进程的多个线程之间使用;其值为非0,表示信号量用于进程间使用。value为重要的参数,表示信号量的初始值

#include <semaphore.h>
int sem_destroy(sem_t *sem);

用来摧毁信号量,参数sem表示信号量的标识符

#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

sem_wait()函数用来执行申请信号量的操作,当申请信号量成功时,信号量的值减1,当信号量的值为0时,此操作将会阻塞,直到其他线程执行释放信号量

sem_trywait()函数与sem_wait()函数类似,唯一的区别在于sem_trywait()函数不会阻塞,当信号量为0时,函数直接返回错误码EAGAIN。

**sem_timewait()**函数同样,多了参数abs_timeout,用来设置时间限制,如果在该时间内,信号量仍然不能申请,那么该函数不会一直阻塞,而是返回错误码ETIMEOUT。

#include <semaphore.h>
int sem_post(sem_t *sem);

用来执行释放信号量的操作,当释放信号量成功时,信号量的值加1

#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);

用于获得当前信号量的值,并将值保存在参数sval中

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

#define MSG_SIZE 100
sem_t sem;


void* thread_func(void *msg)
{   
     sem_wait(&sem);//把信号量减1,此线程从阻塞转为运行
    
    /*收到信号量加一后执行*/
     char *ptr = (char *)msg;;
     while(strcmp("end\n", ptr) != 0)
     {
     int i = 0;
     //把小写字母变成大写
     for(; ptr[i] != '\0'; ++i)
     {
         if(ptr[i] >= 'a' && ptr[i] <= 'z')
         {
         ptr[i] -= 'a' - 'A';
         }
     }
     printf("You input %d characters\n", i-1);
     printf("To Uppercase: %s\n", ptr);
     sem_wait(&sem); //把信号量减1
     }
      pthread_exit(NULL); //退出线程
}

int main()
{
     int res = -1;
     pthread_t thread;
     void *thread_result = NULL;
     char msg[MSG_SIZE];
     
     //初始化信号量,其初值为0
     res = sem_init(&sem, 0, 0);
     if(res == -1)
     {
     perror("semaphore intitialization failed\n");
     exit(EXIT_FAILURE);
     }
    
     //创建线程,并把msg作为线程函数的参数
     res = pthread_create(&thread, NULL, thread_func, msg);
     if(res != 0)
     {
     perror("pthread_create failed\n");
     exit(EXIT_FAILURE);
     }
    
    /*输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了		“end\n”*/
     printf("Input some text. Enter 'end'to finish...\n");
     while(strcmp("end\n", msg) != 0)
     {
     fgets(msg, MSG_SIZE, stdin);
     sem_post(&sem);//把信号量加1
     }
     printf("Waiting for thread to finish...\n");
     
    //等待子线程结束
     res = pthread_join(thread, &thread_result);
     if(res != 0)
     {
     perror("pthread_join failed\n");
     exit(EXIT_FAILURE);
     }
     printf("Thread joined\n");
     sem_destroy(&sem);//清理信号量
     exit(EXIT_SUCCESS);
}

条件变量

条件变量可以看成是互斥锁的补充,因为条件变量需要结合互斥锁一起使用,之所以这样,是因为互斥锁的状态只有锁定和非锁定两种状态,无法决定线程执行先后,有一定的局限。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足

pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 

​ 条件变量的静态初始化,定义变量可以用int

pthread_cond_init(pthread_cond_t*cond,pthread_condattr_t *cond_attr);

​ 功能:初始化一个条件变量
​ cond:指定要初始化的条件变量(指向结构pthread_cond_t的指针)
​ cond_attr:NULL 默认的(用于设置条件变量是进程内还是进程间的)
​ 返回值:0 成功 非0 错误

int pthread_cond_destroy(pthread_cond_t *cond);

​ 功能:销毁一个条件变量
​ cound:指定要销毁的条件变量
​ 返回值:0 成功 非0 错误

int pthread_cond_signal(pthread_cond_t *cond);

​ 功能:启动在等待条件变量变为真的一个线程,每次最多可以给一个线程发送
​ 参数: cond:指定条件变量 返回值:0 成功 非0 错误

int pthread_cond_broadcast(pthread_cond_t *cond);

​ 功能:启动所有的等待条件变量为真的线程
​ 参数: cond:指定条件变量
​ 返回值:0 成功 非0 错误

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

等待期间不会占用cpu,先解锁,然后进入睡眠状态,等待接受信号
​ 功能:等待条件变量为真(无条件等待)
​ 参数:cond:指定等待的条件变量 mutex:等待之前需要解开的锁
​ 返回值:0 成功 非0 错误

wait进入阻塞状态时,必须先对其进行加锁操作,之后再进行解锁操作

为什么需要加锁:

  1. 防止死锁:如果一个线程正在等待条件变量中的条件,但另一个线程占用了锁并且没有释放,那么第一个线程就会一直处于等待状态,这会造成死锁。因此,在使用pthread_cond_wait()等待条件变量之前需要先获得锁,以确保可以成功等待并在满足条件时正确唤醒线程。如果条件变量改变了的话(假如多个唤醒信号所处线程并行执行),那我们就漏掉了这个条件

  2. 确保条件的正确性:条件变量通常是和一些共享数据结构一起使用的,例如一个共享缓冲区。如果没有锁保护,就会出现同步问题。因此,在使用条件变量等待共享数据结构时,需要先获取锁以确保读写共享数据的线程安全,从而保证数据的正确性。

int   pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex, const struct timespec *abstime); //超时等待

​ 功能:超时等待,超时返回错误(计时等待)
cond:指定等待的条件变量 mutex:等待之前需要解开的锁 abstime:指定等待的时间
​ 返回值:0 成功 非0 错误

pthread_cond_signal()函数即可以放在pthread_mutex_lock()函数和pthread_mutex_unlock()函数之间,也可以采用另一种写法,将pthread_cond_signal()函数放在pthread_mutex_lock()函数和pthread_mutex_unlock()函数之后

pthread_mutex_lock(&lock);/*执行加锁操作*/
pthread_cond_wait(&cond, &lock);	//解锁等待,条件来了再加锁
/*线程执行阻塞,此时自动执行解锁,使得其它线程可以获得加锁的权利,当线程收到唤醒信号,函数立即返回,此时在进入临界区之前,再次自动加锁*/
printf("thread2 buf:%s\n", buf);/*临界区*/
sleep(1);
pthread_mutex_unlock(&lock);/*解除互斥锁*/
例子
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/
pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;//init cond
int i = 1; //global

void *thread1(void *junk)
{
    for(i = 1;i<= 9; i++)
    {
        pthread_mutex_lock(&mutex); //加锁
        printf("call thread1 \n");
        if(i%3 == 0)
        {
            pthread_cond_signal(&cond); //send sianal to t_b
            printf("thread1:******i=%d\n", i);
        }
        else
            printf("thread1: %d\n",i);
        pthread_mutex_unlock(&mutex);//解锁
 
		printf("thread1: sleep i=%d\n", i);
         sleep(1);
		printf("thread1: sleep i=%d******end\n", i);
    }
}     

void *thread2(void*junk)
{
    while(i < 9)
    {
        pthread_mutex_lock(&mutex);
        printf("call thread2 \n");
        if(i%3 != 0)
            pthread_cond_wait(&cond,&mutex); //wait
        printf("thread2: %d\n",i);
        pthread_mutex_unlock(&mutex);
 
		printf("thread2: sleep i=%d\n", i);
        sleep(1);
		printf("thread2: sleep i=%d******end\n", i);		
    }
}    


int main(void)
{
    pthread_t t_a;
    pthread_t t_b;//two thread
    pthread_create(&t_a,NULL,thread2,(void*)NULL);    				           pthread_create(&t_b,NULL,thread1,(void*)NULL);
    printf("t_a:0x%x, t_b:0x%x:\n", t_a, t_b);
    pthread_join(t_b,NULL);//wait a_b thread end
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    exit(0);
}     

进程与线程的区别

从形态角度

一个进程可包含一个或多个线程

从调度角度

​ 进程是资源分配的基本单位
​ 线程是处理器调度的独立单位

从虚拟化角度

进程提供两种虚拟机制

​ 虚拟处理器:进程独享处理器的假象
​ 虚拟内存:进程拥有系统内所有内存资源的假象

线程之间可共享虚拟内存,但各自拥有独立虚拟处理器

从通信角度

线程间通信是通过操作共享的数据段实现的(共享进程的数据段)

进程之间如果需要进行数据的传递则需要引入进程间的通信机制来实现(每个进程所操作的地址空间是独立的)

(本文来源学校老师的资料,特此整理方便学习复习)

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ka7ia

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

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

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

打赏作者

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

抵扣说明:

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

余额充值