进程与线程

Linux c处理并发通常会用到进程、线程、多路复用,这几个是最常用的也是最基础的,也准备需要找工作了,整理一下与这些相关的东西,发现光进程与线程就有很多内容,多路复用留到下篇写吧

一、进程

什么是一个进程?在操作系统原理使用这样的术语来描述的:正在运行的程序及其占用的资源(CPU、内存、系统资源等)叫做进程。

进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态就绪态阻塞态。在五态模型中,进程分为创建终止态运行态就绪态阻塞态

Linux内核在启动的最后阶段会创建init进程来执行程序/sbin/init,该进程是系统运行的第一个进程,进程号为 1。Linux下有两个基本的系统调用可以用于创建子进程:fork()和vfork()。如果fork 函数调用失败的原因主要有两个:

1. 系统中已经有太多的进程;

2. 该实际用户 ID 的进程总数超过了系统限制

如果是因为系统限制,可通过getrlimit()、setrlimit()设置系统设置

fork()实现并不执行一个父进程数据段、堆和栈的完全副本拷贝。作为替代,使用了写时复制(CopyOnWrite)技术: 这些数据区域由父子进程共享,内核将他们的访问权限改成只读,如果父进程和子进程中的任何一个试图修改这些区域的时候,内核再为修改区域的那块内存制作一个副本。

vfork()并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit(),于是也就不会引用该地址空间了。不过子进程再调用exec()或exit()之前,他将在父进程的空间中运行,但如果子进程想尝试修改数据域(数据段、堆、栈)都会带来未知的结果,因为他会影响了父进程空间的数据可能会导致父进程的执行异常。此外,vfork()会保证子进程先运行,在他调用了exec或exit()之后父进程才可能被调度运行。如果子进程依赖于父进程的进一步动作,则会导致死锁。 

fork和vfork区别:

1.fork():子进程拷贝父进程的数据段,代码段 。Vfork():子进程与父进程共享数据段

2.fork()父子进程的执行次序不确定,vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec或exit 之后父进程才可能被调度运行。

3.vfork()保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

 

子进程自父进程继承到:

  1. 进程的资格(真实(real)/有效(effective)/已保存(saved) 用户号(UIDs)和组号(GIDs))
  2. 环境(environment)变量
  3. 堆栈
  4. 内存
  5. 打开文件的描述符
  6. 执行时关闭(close-on-exec) 标志
  7. 信号(signal)控制设定
  8. 进程调度类别(scheduler class)
  9. 进程组号
  10. 对话期ID(Session ID)
  11. 当前工作目录
  12. 根目录 (根目录不一定是“/”,它可由chroot函数改变)
  13. 掩模( 文件方式创建屏蔽字(file mode creation mask (umask) )
  14. 资源限 
  15. 控制终端

 

子进程所独有

  1. 进程号不同的父进程号
  2. 自己的文件描述符和目录流的拷贝
  3. 子进程不继承父进程的进程,正文(text), 数据和其它锁定内存(memory locks)
  4. 在tms结构中的系统时间
  5. 资源使用(resource utilizations)设定为0
  6. 阻塞信号集初始化为空集(译者注:原文此处不明确, 译文根据fork函数手册页稍做修改)
  7. 不继承由timer_create函数创建的计时器
  8. 不继承异步输入和输出
  9. 父进程设置的锁

 

僵尸进程和孤儿进程:

如果一个已经终止、但其父进程尚未对其调用wait进行善后处理(获取终止子进程的有关信息如CPU时间片、释放它锁占用的资源如文件描述符等)的进程被称僵死进程(zombie),ps命令将僵死进程的状态打印为Z。如果子进程已经终止,并且是一个僵死进程,则wait立即返回该子进程的状态。所以,我们在编写多进程程序时,最好调用wait()或waitpid()来解决僵尸进程的问题。此外,如果父进程在子进程退出之前退出了,这时候子进程就变成了孤儿进程。当然每一个进程都应该有一个独一无二的父进程,init进程就是这样的一个“慈父”,Linux内核中所有的子进程在变成孤儿进程之后都会被init进程“领养”,这也意味着孤儿进程的父进程最终会变成init进程。

 

进程间通信方式如下:

  1. 信号 ( Sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
     
  2.  管道(Pipe):管道是一种半双工(int fd[2]; pipe(fd) ;一方关闭读端fd[0],一方关闭写端fd[1])的通信方式,数据只能单向流动,而且只能在具有亲缘关系(通常是指父子进程关系)的进程间使用
     
  3.  命名管道FIFO:命名管道(Named PiPe)FIFO,不相关的进程也能交换数据。FIFO不同于管道之处在于它提供一个路径与之关联,以FIFO的文件形式存在于系统中。它在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,通过mknode()系统调用或者mkfifo()函数来建立的。一旦建立,任何进程都可以通过文件名将其打开和进行读写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权,命名管道(Named Pipe)也是半双工(open(FIFO_PATH, O_CREAT|O_WRONLY,0666);open(FIFO_PATH, O_CREAT|O_RDONLY,0666) 一端以只读方式打开,一端以只写方式打开,如果以阻塞模式打开(非阻塞选项O_NONBLOCK,默认是阻塞模式),先开读端而写端还没开,open会阻塞直写端打开)的通信方式,但是它允许无亲缘关系进程间的通信;
     
  4.  命名socket或UNIX域socket(Named Socket或Unix Domain Socket):socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同进程间的进程通信int socket(int domain, int type, int protocol);参数domain指定协议族被置为 AF_UNIX或AF_LOCAL。socket(AF_UNIX, SOCK_STREAM, 0);socket(AF_LOCAL, SOCK_STREAM, 0); ip地址和端口变成path路径,因为仅在同一主机不同进程间通信
     
  5.  信号量(Semaphore):信号量是用来解决进程间的同步与互斥问题的一种进程间通信机制。信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段;
     
  6.  共享内存(Shared Memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号,配合使用,来实现进程间的同步和通信;
     
  7.  消息队列(Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点 

IPC进程间通信:https://blog.csdn.net/caijiwyj/article/details/85255082

二、线程

在操作系统原理的术语中,线程是进程的一条执行路径所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。 

线程与进程的区别:

线程是指进程内的一个执行单元,也是进程内的可调度实体,与进程的区别:

(1)地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间;

(2)资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源

(3)线程是处理器调度的基本单位,但进程不是.

(4)二者均可并发执行.资源分配的单位进程,处理机调度的单位线程

 

线程共享的环境包括:

进程代码段、

进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、

进程打开的文件描述符、

信号的处理、

进程的当前目录和进程用户ID与进程组ID。     

 

线程独有的特性:

进程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:

1.线程ID

每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标  识线程。    

 

2.寄存器组的值

由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。

    

3.线程的堆栈

堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响。

 

4.错误返回码

由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用   后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时   被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。

 

5.线程的信号屏蔽码

由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自   己管理。但所有的线程都共享同样的信号处理器。

 

6.线程的优先级

由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

 

为什么要使用线程:

使用多线程的理由之一是和进程相比,它是一种非常”节俭”的多任务操作方式。

在 Linux 系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。 而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。

使用多线程的理由之二是线程间方便的通信机制。

对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。 线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。 当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为 static 的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

 

线程间通信:

线程间无需特别的手段进行通信,因为线程间可以共享数据结构( pthread_create( pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);通过arg传入同个变量 ),也就是一个全局变量可以被两个线程同时使用。 不过要注意的是线程间需要做好同步:

 

1、信号

Linux 用 pthread_kill 对线程发信号

2、互斥锁以及各种锁机制(如读写锁、自旋锁等)

互斥锁:

在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量。对互斥量进行上锁以后,其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。

int pthread_mutex_lock(pthread_mutex_t* mutex); //上锁;

int pthread_mutex_trylock(pthread_mutex_t* mutex); //只有在互斥被锁住的情况下才阻塞

int pthread_mutex_unlock (pthread_mutex_t* mutex); //解锁

int pthread_mutex_destroy (pthread_mutex_t* mutex); //清除互斥锁

读写锁:

一把读写锁具备三种状态:

    1. 读模式下加锁状态 (读锁)2. 写模式下加锁状态 (写锁)3. 不加锁状态

读写锁特性:    

读写锁是"写模式加锁"时, 解锁前,所有对该锁加锁的线程都会被阻塞。

读写锁是"读模式加锁"时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。

读写锁是"读模式加锁"时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高

    读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。

    读写锁非常适合于对数据结构读的次数远大于写的情况。

主要应用函数:

    pthread_rwlock_init()

    pthread_rwlock_destroy()

    pthread_rwlock_rdlock()

    pthread_rwlock_wrlock()

    pthread_rwlock_tryrdlock()

    pthread_rwlock_trywrlock()

pthread_rwlock_unlock()

自旋锁:

自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁可以用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。内核中自旋锁通常用于处理多个处理器的情况,当一个CPU正访问自旋锁保护的临界区时,临界区将被锁上,其他需要访问此临界区的CPU只能忙等待,直到前面的CPU已访问完临界区,将临界区开锁。自旋锁上锁后让等待线程进行忙等待而不是睡眠阻塞,而信号量是让等待线程睡眠阻塞。自旋锁的忙等待浪费了处理器的时间,但时间通常很短,在1毫秒以下。

int pthread_spin_destroy(pthread_spinlock_t *);

nt pthread_spin_init(pthread_spinlock_t *, int);

int pthread_spin_lock(pthread_spinlock_t *);

int pthread_spin_trylock(pthread_spinlock_t *);

int pthread_spin_unlock(pthread_spinlock_t *);

 

3、条件变量

条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。 这些线程将重新锁定互斥锁并重新测试条件是否满足。

条件变量的相关函数

pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //条件变量结构

int pthread_cond_init(pthread_cond_t cond, pthread_condattr_tcond_attr);

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

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

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,

const struct timespec *abstime);

int pthread_cond_destroy(pthread_cond_t *cond);

 

4、信号量

上面说到过信号量是用来解决同步与互斥问题的一种通信机制,可以通过对信号量进行的两个原子操作(P/V操作)来实现。其中,信号量对应于某一种资源,取一个非负的整形值。信号量值(常用sem_id表示)指的是当前可用的该资源的数量,若等于0则意味着目前没有可用的资源 

 

互斥锁、条件变量和信号量的区别:

互斥锁:互斥,一个线程占用了某个资源,那么其它的线程就无法访问,直到这个线程解锁,其它线程才可以访问。

条件变量:同步,一个线程完成了某一个动作就通过条件变量发送信号告诉别的线程,别的线程再进行某些动作。条件变量必须和互斥锁配合使用。

信号量:同步,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。而且信号量有一个更加强大的功能,信号量可以用作为资源计数器,把信号量的值初始化为某个资源当前可用的数量,使用一个之后递减,归还一个之后递增。

 

临界区与临界资源:

临界资源是在同一时刻只允许有限个(通常只有一个)进程可以访问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器及其它外围设备等)和软件资源(共享代码段、共享结构和变量等)。访问临界资源的代码叫做临界区,临界区本身也会称为临界资源。

 

死锁

死锁是指两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

 

举个例子来说:A君和B君住在一起,但是家里只有一个碗一双筷子,A君打算先拿了筷子再拿碗然后吃饭,而B君则是想先拿碗再拿筷子然后吃饭,A君拿了筷子、B君拿了碗,两个人都很饿都等着对方给自己没拿到的东西才能吃饭,两个人又不肯让出自己手中拿到的东西,就这样赌气一直等一直等,结果两个人都饿扁了,这就是死锁。

 

死锁的四个必要条件:

互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束(碗和筷子都是只能一个人拿)

占有并等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。(A君拿着筷子饥肠辘辘的等着B君把碗给他QAQ)

不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。(A君和B君是舍友,抬头不见低头见,不可以因为这点小事撕破脸皮打架,赌赌气等对方投降就算了,谁料到是两头犟牛)

循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

 

解决死锁的办法:

产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件

 

a、破坏“占有且等待”条件

方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。(第二天A君一回来就迅速同时拿了碗和筷)

优点:简单易实施且安全。

缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。使进程经常发生饥饿现象。

方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。

 

b、破坏“不可抢占”条件

当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。(A君心想既然没拿到碗,就给筷子给他先吃饭吧)

 

c、破坏“循环等待”条件

可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。(A君和B君约定好了谁先抢到筷子谁就可以拿碗吃饭)

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值