精华内容
下载资源
问答
  • 这是前两篇的地址,可以去看代码解释 ...首先上本机实测截图 如果对linux基础命令都不太熟悉的话,那就先学习基础命令吧,要不然你也不清楚我敲这几个简单的命令是为啥子 下面上服务端的代码 #! /usr/bin/env python3...
  • linux fork多线程的问题

    2016-03-24 02:46:04
    各位大师,我看fork函数,这个问题困扰了好久。请各位大师赐教。 既然replicated了other pthreads objects,为什么child process 只有 a single thread呢? ![图片说明]...
  • fork多线程下的使用

    2020-11-30 15:59:24
    谨慎使用多线程中的fork 2015-02-07 19:23origins 阅读(16611) 评论(4)编辑收藏 前言 在单核时代,大家所编写的程序都是单进程/单线程程序。随着计算机硬件技术的发展,进入了多核时代后,为了降低响应时间,重复...

    多线程与多进程的选择

    关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有这么简单了,选的不好,会让你深受其害。

    经常在网络上看到有的XDJM问“多进程好还是多线程好?”、“Linux下用多进程还是多线程?”等等期望一劳永逸的问题,我只能说:没有最好,只有更好。根据实际情况来判断,哪个更加合适就是哪个好。

    我们按照多个不同的维度,来看看多线程和多进程的对比(注:因为是感性的比较,因此都是相对的,不是说一个好得不得了,另外一个差的无法忍受)。

    在这里插入图片描述
    在这里插入图片描述

    1)需要频繁创建销毁的优先用线程

    原因请看上面的对比。

    这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的

    2)需要进行大量计算的优先使用线程

    所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。

    这种原则最常见的是图像处理、算法处理。

    3)强相关的处理用线程,弱相关的处理用进程

    什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。

    一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。

    当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

    4)可能要扩展到多机分布的用进程,多核分布的用线程

    原因请看上面对比。

    5)都满足需求的情况下,用你最熟悉、最拿手的方式

    至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。

    需要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。
    消耗资源:

    从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

    线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

    谨慎使用多线程中的fork

    在单核时代,大家所编写的程序都是单进程/单线程程序。随着计算机硬件技术的发展,进入了多核时代后,为了降低响应时间,重复充分利用多核cpu的资源,使用多进程编程的手段逐渐被人们接受和掌握。然而因为创建一个进程代价比较大,多线程编程的手段也就逐渐被人们认可和喜爱了。

    进程与线程模型

    进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的context中的。context是由程序正确运行所需的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器(PC)、环境变量以及打开的文件描述符的集合。

    进程主要提供给上层的应用程序两个抽象:

    • 一个独立的逻辑控制流,它提供一个假象,好像我们程序独占的使用处理器。
    • 一个私有的虚拟地址空间,它提供一个假象,好像我们的程序独占的使用存储器系统。

    线程,就是运行在进程context中的逻辑流。线程由内核自动调度。每个线程都有它自己的线程context,包括一个唯一的整数线程ID、栈、栈指针、程序计数器(PC)、通用目的寄存器和条件码。每个线程和运行在同一进程内的其他线程一起共享进程context的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)、读/写数据、堆以及所有的共享库代码和数据区域组成。线程也同样共享打开文件的集合。

    即进程是资源管理的最小单位,而线程是程序执行的最小单位。

    在linux系统中,posix线程可以“看做”为一种轻量级的进程,pthread_create创建线程和fork创建进程都是在内核中调用__clone函数创建的,只不过创建线程或进程的时候选项不同,比如是否共享虚拟地址空间、文件描述符等。

    fork与多线程

    我们知道通过fork创建的一个子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈等。子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着子进程可以读写父进程中任何打开的文件,父进程和子进程之间最大的区别在于它们有着不同的PID。

    但是有一点需要注意的是,在Linux中,fork的时候只复制当前线程到子进程,在fork(2)-Linux Man Page中有着这样一段相关的描述:

    The child process is created with a single thread--the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.

    也就是说除了调用fork的线程外,其他线程在子进程中“蒸发”了。

    这就是多线程中fork所带来的一切问题的根源所在了。

    互斥锁

    互斥锁,就是多线程fork大部分问题的关键部分。

    在大多数操作系统上,为了性能的因素,锁基本上都是实现在用户态的而非内核态(因为在用户态实现最方便,基本上就是通过原子操作或者之前文章中提到的memory barrier实现的),所以调用fork的时候,会复制父进程的所有锁到子进程中。

    问题就出在这了。从操作系统的角度上看,对于每一个锁都有它的持有者,即对它进行lock操作的线程。假设在fork之前,一个线程对某个锁进行的lock操作,即持有了该锁,然后另外一个线程调用了fork创建子进程。可是在子进程中持有那个锁的线程却"消失"了,从子进程的角度来看,这个锁被“永久”的上锁了,因为它的持有者“蒸发”了。

    那么如果子进程中的任何一个线程对这个已经被持有的锁进行lock操作话,就会发生死锁。

    当然了有人会说可以在fork之前,让准备调用fork的线程获取所有的锁,然后再在fork出的子进程的中释放每一个锁。先不说现实中的业务逻辑以及其他因素允不允许这样做,这种做法会带来一个问题,那就是隐含了一种上锁的先后顺序,如果次序和平时不同,就会发生死锁。

    如果你说自己一定可以按正确的顺序上锁而不出错的话,还有一个隐含的问题是你所不能控制的,那就是库函数。

    因为你不能确定你所用到的所有库函数都不会使用共享数据,即他们都是完全线程安全的。有相当一部分线程安全的库函数都是在内部通过持有互斥锁的方式来实现的,比如几乎所有程序都会用到的C/C++标准库函数malloc、printf等等。

    比如一个多线程程序在fork之前难免会分配动态内存,这就必然会用到malloc函数;而在fork之后的子进程中也难免要分配动态内存,这也同样要用到malloc,可这却是不安全的,因为有可能malloc内部的锁已经在fork之前被某一个线程所持有了,而那个线程却在子进程中消失了。

    exec与文件描述符

    按照上文的分析,似乎多线程中在fork出的子进程中立刻调用exec函数是唯一明智的选择了,其实即使这样做还是有一点不足。因为子进程会继承父进程中所有已打开的文件描述符,所以在执行exec之前子进程仍然可以读写父进程中的文件,但如果你不希望子进程能读写父进程里的某个已打开的文件该怎么办?

    或许fcntl设置文件属性是一种办法:

    1

    2

    3

    4

    5

    6

    int fd = open("file", O_RDWR | O_CREAT);

    if (fd < 0)

    {

        perror("open");

    }

    fcntl(fd, F_SETFD, FD_CLOEXEC);

    但是如果在open打开file文件之后,调用fcntl设置CLOEXEC属性之前有其他线程fork出了子进程了的话,这个子进程仍然是可以读写file文件。如果用锁的话,就又回到了上文所讨论的情况了。

    从Linux 2.6.23版本的内核开始,我们可以在open中设置O_CLOEXEC标志了,相当于“打开文件再设置CLOEXEC”成为了一个原子操作。这样在fork出的子进程执行exec之前就不能读写父进程中已打开的文件了。

    pthread_atfork

    如果你不幸真的碰到了一个要解决多线程中fork的问题的时候,可以尝试使用pthread_atfork:

    1

    int pthread_atfork(void (*prepare)(void), void (*parent)void(), void (*child)(void));

    • prepare处理函数由父进程在fork创建子进程前调用,这个函数的任务是获取父进程定义的所有锁。
    • parent处理函数是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的。它的任务是对prepare获取的所有锁解锁。
    • child处理函数在fork返回之前在子进程环境中调用,与parent处理函数一样,它也必须解锁所有prepare中所获取的锁。

    因为子进程继承的是父进程的锁的拷贝,所有上述并不是解锁了两次,而是各自独自解锁。可以多次调用pthread_atfork函数从而设置多套fork处理程序,但是使用多个处理程序的时候。处理程序的调用顺序并不相同。parent和child是以它们注册时的顺序调用的,而prepare的调用顺序与注册顺序相反。这样可以允许多个模块注册它们自己的处理程序并且保持锁的层次(类似于多个RAII对象的构造析构层次)。

    需要注意的是pthread_atfork只能清理锁,但不能清理条件变量。在有些系统的实现中条件变量不需要清理。但是在有的系统中,条件变量的实现中包含了锁,这种情况就需要清理。但是目前并没有清理条件变量的接口和方法。

    结语

    • 在多线程程序中最好只用fork来执行exec函数,不要对fork出的子进程进行其他任何操作。
    • 如果确定要在多线程中通过fork出的子进程执行exec函数,那么在fork之前打开文件描述符时需要加上CLOEXEC标志。

    参考文献

    1. Randal E.Bryant, David O'Hallaron. 深入理解计算机系统(第2版). 机械工业出版社, 2010
    2. W.Richard Stevens. UNIX环境高级编程(第3版), 人民邮电出版社, 2014
    3. Linux Man Page. fork(2)
    4. Damian Pietras. Threads and fork(): think twice before mixing them, 2009
    5. 云风. 极不和谐的 fork 多线程程序, 2011
    展开全文
  • 谨慎使用多线程中的fork 在单核时代,大家所编写的程序都是单进程/单线程程序。随着计算机硬件技术的发展,进入了多核时代后,为了降低响应时间,重复充分利用多核cpu的资源,使用多进程编程的手段逐渐被人们接受和...

    谨慎使用多线程中的fork

    在单核时代,大家所编写的程序都是单进程/单线程程序。随着计算机硬件技术的发展,进入了多核时代后,为了降低响应时间,重复充分利用多核cpu的资源,使用多进程编程的手段逐渐被人们接受和掌握。然而因为创建一个进程代价比较大,多线程编程的手段也就逐渐被人们认可和喜爱了。

    记得在我刚刚学习线程进程的时候就想,为什么很少见人把多进程和多线程结合起来使用呢,把二者结合起来不是更好吗?现在想想当初真是too young too simple,后文就主要讨论一下这个问题。

    进程与线程模型

    进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的context中的。context是由程序正确运行所需的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器(PC)、环境变量以及打开的文件描述符的集合。

    进程主要提供给上层的应用程序两个抽象:

    • 一个独立的逻辑控制流,它提供一个假象,好像我们程序独占的使用处理器。
    • 一个私有的虚拟地址空间,它提供一个假象,好像我们的程序独占的使用存储器系统。

    线程,就是运行在进程context中的逻辑流。线程由内核自动调度。每个线程都有它自己的线程context,包括一个唯一的整数线程ID、栈、栈指针、程序计数器(PC)、通用目的寄存器和条件码。每个线程和运行在同一进程内的其他线程一起共享进程context的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)、读/写数据、堆以及所有的共享库代码和数据区域组成。线程也同样共享打开文件的集合。

    即进程是资源管理的最小单位,而线程是程序执行的最小单位。

    在linux系统中,posix线程可以“看做”为一种轻量级的进程,pthread_create创建线程和fork创建进程都是在内核中调用__clone函数创建的,只不过创建线程或进程的时候选项不同,比如是否共享虚拟地址空间、文件描述符等。

    fork与多线程

    我们知道通过fork创建的一个子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈等。子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着子进程可以读写父进程中任何打开的文件,父进程和子进程之间最大的区别在于它们有着不同的PID。

    但是有一点需要注意的是,在Linux中,fork的时候只复制当前线程到子进程,在fork(2)-Linux Man Page中有着这样一段相关的描述:

    The child process is created with a single thread–the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.

    也就是说除了调用fork的线程外,其他线程在子进程中“蒸发”了。

    这就是多线程中fork所带来的一切问题的根源所在了。

    互斥锁

    互斥锁,就是多线程fork大部分问题的关键部分。

    在大多数操作系统上,为了性能的因素,锁基本上都是实现在用户态的而非内核态(因为在用户态实现最方便,基本上就是通过原子操作或者之前文章中提到的memory barrier实现的),所以调用fork的时候,会复制父进程的所有锁到子进程中。

    问题就出在这了。从操作系统的角度上看,对于每一个锁都有它的持有者,即对它进行lock操作的线程。假设在fork之前,一个线程对某个锁进行的lock操作,即持有了该锁,然后另外一个线程调用了fork创建子进程。可是在子进程中持有那个锁的线程却"消失"了,从子进程的角度来看,这个锁被“永久”的上锁了,因为它的持有者“蒸发”了。

    那么如果子进程中的任何一个线程对这个已经被持有的锁进行lock操作话,就会发生死锁。

    当然了有人会说可以在fork之前,让准备调用fork的线程获取所有的锁,然后再在fork出的子进程的中释放每一个锁。先不说现实中的业务逻辑以及其他因素允不允许这样做,这种做法会带来一个问题,那就是隐含了一种上锁的先后顺序,如果次序和平时不同,就会发生死锁。

    如果你说自己一定可以按正确的顺序上锁而不出错的话,还有一个隐含的问题是你所不能控制的,那就是库函数。

    因为你不能确定你所用到的所有库函数都不会使用共享数据,即他们都是完全线程安全的。有相当一部分线程安全的库函数都是在内部通过持有互斥锁的方式来实现的,比如几乎所有程序都会用到的C/C++标准库函数malloc、printf等等。

    比如一个多线程程序在fork之前难免会分配动态内存,这就必然会用到malloc函数;而在fork之后的子进程中也难免要分配动态内存,这也同样要用到malloc,可这却是不安全的,因为有可能malloc内部的锁已经在fork之前被某一个线程所持有了,而那个线程却在子进程中消失了。

    exec与文件描述符

    按照上文的分析,似乎多线程中在fork出的子进程中立刻调用exec函数是唯一明智的选择了,其实即使这样做还是有一点不足。因为子进程会继承父进程中所有已打开的文件描述符,所以在执行exec之前子进程仍然可以读写父进程中的文件,但如果你不希望子进程能读写父进程里的某个已打开的文件该怎么办?

    或许fcntl设置文件属性是一种办法:

    int fd = open("file", O_RDWR | O_CREAT);
    if (fd < 0)
    {
        perror("open");
    }
    fcntl(fd, F_SETFD, FD_CLOEXEC);
    

    但是如果在open打开file文件之后,调用fcntl设置CLOEXEC属性之前有其他线程fork出了子进程了的话,这个子进程仍然是可以读写file文件。如果用锁保护文件的话,持有文件锁的子线程也许消失于子进程。从而就又回到了上文所讨论的情况了。

    从Linux 2.6.23版本的内核开始,我们可以在open中设置O_CLOEXEC标志了,相当于“打开文件再设置CLOEXEC”成为了一个原子操作。这样在fork出的子进程执行exec之前就不能读写父进程中已打开的文件了。

    pthread_atfork

    如果你不幸真的碰到了一个要解决多线程中fork的问题的时候,可以尝试使用pthread_atfork:

    int pthread_atfork(void (*prepare)(void), void (*parent)void(), void (*child)(void));
    
    • prepare处理函数由父进程在fork创建子进程前调用,这个函数的通常任务是获取父进程定义的所有锁。
    • parent处理函数是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的。它的通常任务是对prepare获取的所有锁解锁。
    • child处理函数在fork返回之前在子进程环境中调用,与parent处理函数一样,它也必须解锁所有prepare中所获取的锁。

    因为子进程继承的是父进程的锁的拷贝,所有上述并不是解锁了两次,而是各自独自解锁。可以多次调用pthread_atfork函数从而设置多套fork处理程序,但是使用多个处理程序的时候。处理程序的调用顺序并不相同。parent和child是以它们注册时的顺序调用的,而prepare的调用顺序与注册顺序相反。这样可以允许多个模块注册它们自己的处理程序并且保持锁的层次(类似于多个RAII对象的构造析构层次)。比如先获取A锁,再获取B锁,那么释放的时候就是先释放B锁,再释放A锁。

    需要注意的是pthread_atfork只能清理锁,但不能清理条件变量。在有些系统的实现中条件变量不需要清理。但是在有的系统中,条件变量的实现中包含了锁,这种情况就需要清理。但是目前并没有清理条件变量的接口和方法。

    结语

    • 在多线程程序中最好只用fork来执行exec函数,不要对fork出的子进程进行其他任何操作。
    • 如果确定要在多线程中通过fork出的子进程执行exec函数,那么在fork之前打开文件描述符时需要加上CLOEXEC标志。

    参考文献

    • Randal E.Bryant, David O’Hallaron. 深入理解计算机系统(第2版). 机械工业出版社, 2010
    • W.Richard Stevens. UNIX环境高级编程(第3版), 人民邮电出版社, 2014
    • Linux Man Page. fork(2) http://linux.die.net/man/2/fork
    • Damian Pietras. Threads and fork(): think twice before mixing them, 2009 http://www.linuxprogrammingblog.com/threads-and-fork-think-twice-before-using-them
    • 云风. 极不和谐的 fork 多线程程序, 2011 http://blog.codingnow.com/2011/01/fork_multi_thread.html
    展开全文
  • fork()与多线程

    千次阅读 2018-10-17 11:31:24
     在操作系统的基本概念中进程是程序的一次执行,且是拥有资源的最小单位和调度单位(在引入线程的操作系统中,线程是最小的调度单位)。在Linux系统中创建进程有两种方式:一是由操作系统创建,二是由父进程创建...

    https://blog.csdn.net/together_cz/article/details/74762850 

    一、fork()函数

        在操作系统的基本概念中进程是程序的一次执行,且是拥有资源的最小单位和调度单位(在引入线程的操作系统中,线程是最小的调度单位)。在Linux系统中创建进程有两种方式:一是由操作系统创建,二是由父进程创建进程(通常为子进程)。系统调用函数fork()是创建一个新进程的唯一方式,当然vfork()也可以创建进程,但是实际上其还是调用了fork()函数。fork()函数是Linux系统中一个比较特殊的函数,其一次调用会有两个返回值,下面是fork()函数的声明:

    #include <unistd.h>

     

    // On success, The PID of the process is returned in the parent, and 0 is returned in the child. On failure,

    // -1 is returned in the parent, no child process is created, and errno is set appropriately.

    pid_t fork (void);

     

        当程序调用fork()函数并返回成功之后,程序就将变成两个进程,调用fork()者为父进程,后来生成者为子进程。这两个进程将执行相同的程序文本,但却各自拥有不同的栈段、数据段以及堆栈拷贝。子进程的栈、数据以及栈段开始时是父进程内存相应各部分的完全拷贝,因此它们互不影响。从性能方面考虑,父进程到子进程的数据拷贝并不是创建时就拷贝了的,而是采用了写时拷贝(copy-on -write)技术来处理。调用fork()之后,父进程与子进程的执行顺序是我们无法确定的(即调度进程使用CPU),意识到这一点极为重要,因为在一些设计不好的程序中会导致资源竞争,从而出现不可预知的问题。下图为写时拷贝技术处理前后的示意图:

              


        在Linux系统中,常常存在许多对文件的操作,fork()的执行将会对文件操作带来一些小麻烦。由于子进程会将父进程的大多数数据拷贝一份,这样在文件操作中就意味着子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于dup()函数调用,因此父、子进程中对应的文件描述符均指向相同的打开的文件句柄,而且打开的文件句柄包含着当前文件的偏移量以及文件状态标志,所以在父子进程中处理文件时要考虑这种情况,以避免文件内容出现混乱或者别的问题。下图为执行fork()调用后文件描述符的相关处理及其变化:

                    

     

     

     

    二、线程

        与进程类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程中可以包含多个线程,同一个程序中的所有线程均会独立执行,且共享同一份全局内存区域,其中包括初始化数据段(initialized data),未初始化数据段(uninitialized data),以及堆内存段(heap segment)。在多处理器环境下,多个线程可以同时执行,如果线程数超过了CPU的个数,那么每个线程的执行顺序将是无法确定的,因此对于一些全局共享数据据需要使用同步机制来确保其的正确性。

        在系统中,线程也是稀缺资源,一个进程能同时创建多少个线程这取决于地址空间的大小和内核参数,一台机器可以同时并发运行多少个线程也受限于CPU的数目。在进行程序设计时,我们应该精心规划线程的个数,特别是根据机器CPU的数目来设置工作线程的数目,并为关键任务保留足够的计算资源。如果你设计的程序在背地里启动了额外的线程来执行任务,那这也属于资源规划漏算的情况,从而影响关键任务的执行,最终导致无法达到预期的性能。很多程序中都存在全局对象,这些全局对象的初始化工作都是在进入main()函数之前进行的,为了能保证全局对象的安全初始化(按顺序的),因此在程序进入main()函数之前应该避免线程的创建,从而杜绝未知错误的发生。

     

    三、fork()与多线程

        在程序中fork()与多线程的协作性很差,这是POSIX系列操作系统的历史包袱。因为长期以来程序都是单线程的,fork()运转正常。当20世纪90年代初期引入线程之后,fork()的适用范围就大为缩小了。

        在多线程执行的情况下调用fork()函数,仅会将发起调用的线程复制到子进程中。(子进程中该线程的ID与父进程中发起fork()调用的线程ID是一样的,因此,线程ID相同的情况有时我们需要做特殊的处理。)也就是说不能同时创建出于父进程一样多线程的子进程。其他线程均在子进程中立即停止并消失,并且不会为这些线程调用清理函数以及针对线程局部存储变量的析构函数。这将导致下列一些问题:

    1. 虽然只将发起fork()调用的线程复制到子进程中,但全局变量的状态以及所有的pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留,这就造成一个危险的局面。例如:一个线程在fork()被调用前锁定了某个互斥量,且对某个全局变量的更新也做到了一半,此时fork()被调用,所有数据及状态被拷贝到子进程中,那么子进程中对该互斥量就无法解锁(因为其并非该互斥量的属主),如果再试图锁定该互斥量就会导致死锁,这是多线程编程中最不愿意看到的情况。同时,全局变量的状态也可能处于不一致的状态,因为对其更新的操作只做到了一半对应的线程就消失了。fork()函数被调用之后,子进程就相当于处于signal handler之中,此时就不能调用线程安全的函数(用锁机制实现安全的函数),除非函数是可重入的,而只能调用异步信号安全(async-signal-safe)的函数。fork()之后,子进程不能调用:

    • malloc(3)。因为malloc()在访问全局状态时会加锁。
    • 任何可能分配或释放内存的函数,包括new、map::insert()、snprintf() ……
    • 任何pthreads函数。你不能用pthread_cond_signal()去通知父进程,只能通过读写pipe(2)来同步。
    • printf()系列函数,因为其他线程可能恰好持有stdout/stderr的锁。
    • 除了man 7 signal中明确列出的“signal安全”函数之外的任何函数。

    2. 因为并未执行清理函数和针对线程局部存储数据的析构函数,所以多线程情况下可能会导致子进程的内存泄露。另外,子进程中的线程可能无法访问(父进程中)由其他线程所创建的线程局部存储变量,因为(子进程)没有任何相应的引用指针。

     

        由于这些问题,推荐在多线程程序中调用fork()的唯一情况是:其后立即调用exec()函数执行另一个程序,彻底隔断子进程与父进程的关系。由新的进程覆盖掉原有的内存,使得子进程中的所有pthreads对象消失。

        对于那些必须执行fork(),而其后又无exec()紧随其后的程序来说,pthreads API提供了一种机制:fork()处理函数。利用函数pthread_atfork()来创建fork()处理函数。pthread_atfork()声明如下:

    #include <pthread.h>

     

    // Upon successful completion, pthread_atfork() shall return a value of zero; otherwise, an error number shall be returned to indicate the error.

    // @prepare 新进程产生之前被调用

    // @parent  新进程产生之后在父进程被调用

    // @child    新进程产生之后,在子进程被调用

    int pthread_atfork (void (*prepare) (void), void (*parent) (void), void (*child) (void));

     

    该函数的作用就是往进程中注册三个函数,以便在不同的阶段调用,有了这三个参数,我们就可以在对应的函数中加入对应的处理功能。同时需要注意的是,每次调用pthread_atfork()函数会将prepare添加到一个函数列表中,创建子进程之前会(按与注册次序相反的顺序)自动执行该函数列表中函数。parent与child也会被添加到一个函数列表中,在fork()返回前,分别在父子进程中自动执行(按注册的顺序)。具体事例可参考:http://blog.chinaunix.net/uid-26885237-id-3210394.html

     

    四、总结

        fork()函数的调用会导致在子进程中除调用线程外的其它线程全都终止执行并消失,因此在多线程的情况下会导致死锁和内存泄露的情况。在进行多线程编程的时候尽量避免fork()的调用,同时在程序在进入main函数之前应避免创建线程,因为这会影响到全局对象的安全初始化。线程不应该被强行终止,因为这样它就没有机会调用清理函数来做相应的操作,同时也就没有机会来释放已被锁住的锁,如果另一线程对未被解锁的锁进行加锁,那么将会立即发生死锁,从而导致程序无法正常运行。

     

     

    参考

    [1] Linux/UNIX系统编程手册(上)

    [2] Linux多线程服务端编程使用muduo C++网络库

    [3] http://blog.chinaunix.net/uid-26885237-id-3210394.html

     

    转载自:http://blog.csdn.net/cywosp/article/details/27316803

     

     

    IO模式设置网络编程常见问题总结—IO模式设置,阻塞与非阻塞的比较,recv参数对性能的影响—O_NONBLOCK(open使用)、IPC_NOWAIT(msgrcv)、MSG_DONTWAIT(recv使用)

    非阻塞IO 和阻塞IO:

           在网络编程中对于一个网络句柄会遇到阻塞IO 和非阻塞IO 的概念, 这里对于这两种socket 先做一下说明:
           基本概念:

                  阻塞IO::

                         socket 的阻塞模式意味着必须要做完IO 操作(包括错误)才会

                         返回。

                  非阻塞IO::

                         非阻塞模式下无论操作是否完成都会立刻返回,需要通过其他方

                         式来判断具体操作是否成功。


    IO模式设置:

                                                   SOCKET
           对于一个socket 是阻塞模式还是非阻塞模式有两种方式来处理::

           方法1、fcntl 设置;用F_GETFL获取flags,用F_SETFL设置flags|O_NONBLOCK;

           方法2、recv,send 系列的参数。(读取,发送时,临时将sockfd或filefd设置为非阻塞)

                                                方法一的实现

     fcntl 函数可以将一个socket 句柄设置成非阻塞模式: 
          flags = fcntl(sockfd, F_GETFL, 0);                       //获取文件的flags值。

          fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);   //设置成非阻塞模式;

          flags  = fcntl(sockfd,F_GETFL,0);

          fcntl(sockfd,F_SETFL,flags&~O_NONBLOCK);    //设置成阻塞模式;

          设置之后每次的对于sockfd 的操作都是非阻塞的。

                                               方法二的实现

        recv, send 函数的最后有一个flag 参数可以设置成MSG_DONTWAIT

                 临时将sockfd 设置为非阻塞模式,而无论原有是阻塞还是非阻塞。

        recv(sockfd, buff, buff_size,MSG_DONTWAIT);     //非阻塞模式的消息发送

        send(scokfd, buff, buff_size, MSG_DONTWAIT);   //非阻塞模式的消息接受

     

     

                                                        普通文件

            对于文件的阻塞模式还是非阻塞模式::

            方法1、open时,使用O_NONBLOCK;

            方法2、fcntl设置,使用F_SETFL,flags|O_NONBLOCK;

                                      

     

                                                  消息队列

            对于消息队列消息的发送与接受::

            //非阻塞  msgsnd(sockfd,msgbuf,msgsize(不包含类型大小),IPC_NOWAIT)

            //阻塞     msgrcv(scokfd,msgbuf,msgsize(**),msgtype,IPC_NOWAIT);

     

                                                                  

     

                                                                      读                

    阻塞与非阻塞读的区别:  //阻塞和非阻塞的区别在于没有数据到达的时候是否立刻返回.

    读(read/recv/msgrcv):

           读的本质来说其实不能是读,在实际中, 具体的接收数据不是由这些调用来进行,是由于系统底层自动完成的。read 也好,recv 也好只负责把数据从底层缓冲copy 到我们指定的位置.

           对于读来说(read, 或者recv) ::

    阻塞情况下::

           在阻塞条件下,read/recv/msgrcv的行为::

           1、如果没有发现数据在网络缓冲中会一直等待,

           2、当发现有数据的时候会把数据读到用户指定的缓冲区,但是如果这个时候读到的数据量比较少,比参数中指定的长度要小,read 并不会一直等待下去,而是立刻返回。

           read 的原则::是数据在不超过指定的长度的时候有多少读多少,没有数据就会一直等待。

           所以一般情况下::我们读取数据都需要采用循环读的方式读取数据,因为一次read 完毕不能保证读到我们需要长度的数据,

           read 完一次需要判断读到的数据长度再决定是否还需要再次读取。

    非阻塞情况下::

           在非阻塞的情况下,read 的行为::

           1、如果发现没有数据就直接返回,

           2、如果发现有数据那么也是采用有多少读多少的进行处理.

                 所以::read 完一次需要判断读到的数据长度再决定是否还需要再次读取。

     

    对于读而言::   阻塞和非阻塞的区别在于没有数据到达的时候是否立刻返回.
           recv 中有一个MSG_WAITALL 的参数::

           recv(sockfd, buff, buff_size, MSG_WAITALL),
           在正常情况下recv 是会等待直到读取到buff_size 长度的数据,但是这里的WAITALL 也只是尽量读全,在有中断的情况下recv 还是可能会被打断,造成没有读完指定的buff_size的长度。

           所以即使是采用recv + WAITALL 参数还是要考虑是否需要循环读取的问题,在实验中对于多数情况下recv (使用了MSG_WAITALL)还是可以读完buff_size,

           所以相应的性能会比直接read 进行循环读要好一些。

     

    注意::      //使用MSG_WAITALL时,sockfd必须处于阻塞模式下,否则不起作用。

                   //所以MSG_WAITALL不能和MSG_NONBLOCK同时使用。

           要注意的是使用MSG_WAITALL的时候,sockfd 必须是处于阻塞模式下,否则WAITALL不能起作用。

     

     

                                                                             写 

     

     

    阻塞与非阻塞写的区别:     //
    写(send/write/msgsnd)::

           写的本质也不是进行发送操作,而是把用户态的数据copy 到系统底层去,然后再由系统进行发送操作,send,write返回成功,只表示数据已经copy 到底层缓冲,而不表示数据已经发出,更不能表示对方端口已经接收到数据.
           对于write(或者send)而言,

    阻塞情况下::                 //阻塞情况下,write会将数据发送完。(不过可能被中断)

           在阻塞的情况下,是会一直等待,直到write 完,全部的数据再返回.这点行为上与读操作有所不同。

            原因::

                  读,究其原因主要是读数据的时候我们并不知道对端到底有没有数据,数据是在什么时候结束发送的,如果一直等待就可能会造成死循环,所以并没有去进行这方面的处理;

                  写,而对于write, 由于需要写的长度是已知的,所以可以一直再写,直到写完.不过问题是write 是可能被打断吗,造成write 一次只write 一部分数据, 所以write 的过程还是需要考虑循环write, 只不过多数情况下一次write 调用就可能成功.

     

    非阻塞写的情况下::     //

           非阻塞写的情况下,是采用可以写多少就写多少的策略.与读不一样的地方在于,有多少读多少是由网络发送的那一端是否有数据传输到为标准,但是对于可以写多少是由本地的网络堵塞情况为标准的,在网络阻塞严重的时候,网络层没有足够的内存来进行写操作,这时候就会出现写不成功的情况,阻塞情况下会尽可能(有可能被中断)等待到数据全部发送完毕, 对于非阻塞的情况就是一次写多少算多少,没有中断的情况下也还是会出现write 到一部分的情况.

    本文为转载  http://blog.csdn.net/s_k_yliu/article/details/6657956

    展开全文
  • 单线程场景 对于fork系统调用,我们知道是linux下创建子进程的一种方式。fork调用一次,对于程序看来,是“返回两次”。这里其实理解为fork调用中,已经创建出了子进程,父子进程分别分从...对于多线程场景中,例...

    单线程场景

    对于fork系统调用,我们知道是linux下创建子进程的一种方式。fork调用一次,对于程序看来,是“返回两次”。这里其实理解为fork调用中,已经创建出了子进程,父子进程分别分从fork调用中返回。父进程需要知道子进程的进程ID,所以返回值大于0的是父进程,而子进程返回0即可,子进程可以通过getpid获取自身进程ID和getppid获取父进程ID。

    多线程场景

    对于多线程场景中,例如一个10个线程的进程中。如果某个线程调用了fork,新的子进程中会有多少个线程?请思考30秒。
    正确答案是:直接man fork即可看到。

    Note the following further points:
    The child process is created with a single thread—the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.

    结论:只有当前线程会被复制到子线程中。

    为什么fork只复制当前线程

    试想,如果多个线程都被复制到了子进程中,除了当前线程能通过fork返回值判断,其他线程怎么感知到自己被复制到了子线程呢?从逻辑上就比较容易推断出,这种多线程同时复制到子进制的fork方案是不合理的。

    锁资源

    锁资源例如mutex,同样也是会被复制到子进程中。那么就会出现一种情况,当前线程的mutex的状态被复现到了子进程中。如果mutext是被加锁状态,则在子进程如果尝试对mutext进行加锁的时候,会导致子锁的产生。如下代码所示:

    #include <stdio.h>
    #include <time.h>
    #include <pthread.h>
    #include <unistd.h>
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    void *fun(void *arg)
    {
        printf("pid = %d begin doit ...\n", static_cast<int>(getpid()));
        pthread_mutex_lock(&mutex);
        struct timespec ts = {2, 0};
        nanosleep(&ts, NULL);
        pthread_mutex_unlock(&mutex);
        printf("pid = %d end doit ...\n", static_cast<int>(getpid()));
        return NULL;
    }
    int main(void)
    {
        printf("pid = %d Entering main ...\n", static_cast<int>(getpid()));
        pthread_t tid;
        pthread_create(&tid, NULL, fun, NULL);
        struct timespec ts = {1, 0};
        nanosleep(&ts, NULL);
        if (fork() == 0)
        {
            fun(NULL);
        }
        pthread_join(tid, NULL);
        printf("pid = %d Exiting main ...\n", static_cast<int>(getpid()));
        return 0;
    }
    

    解决方案

    通过int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)),这个系统调用解决这类问题。
    主要流程说明:
    1、在prepare函数中去获取父进程中所有的锁资源。
    2、在parent函数中去释放所有的锁资源。
    3、在child函数中去释放所有的锁资源。

    这样即可保证,父进程的锁不受到影响,子进程的初始化的锁状态是未锁定的。
    参考以下代码:

    #include <stdio.h>
    #include <time.h>
    #include <pthread.h>
    #include <unistd.h>
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    void *fun(void *arg)
    {
        printf("pid = %d begin doit ...\n", static_cast<int>(getpid()));
        pthread_mutex_lock(&mutex);
        struct timespec ts = {2, 0}; 
        nanosleep(&ts, NULL);
        pthread_mutex_unlock(&mutex);
        printf("pid = %d end doit ...\n", static_cast<int>(getpid()));
        return NULL;
    }
    
    void prepare() {
        printf("begin prepare\n"); 
        pthread_mutex_lock(&mutex);
        printf("after prepare\n");
    }
    
    void parent() {
        printf("begin parent\n");
        pthread_mutex_unlock(&mutex);
        printf("after parent\n");
    
    }
    
    void child() {
        printf("begin child\n"); 
        pthread_mutex_unlock(&mutex);
        printf("after child\n");
    }
    
    int main(void)
    {
        pthread_atfork(prepare, parent, child);
        printf("pid = %d Entering main ...\n", static_cast<int>(getpid()));
        pthread_t tid;
        pthread_create(&tid, NULL, fun, NULL);
        
        if (fork() == 0)
        {   
            printf("child process pid[%d]\n", getpid());
            fun(NULL);
        }   
        pthread_join(tid, NULL);
        printf("pid = %d Exiting main ...\n", static_cast<int>(getpid()));
        return 0;
    }
    
    展开全文
  •  在操作系统的基本概念中进程是程序的一次执行,且是拥有资源的小单位和调度单位(在引入线程的操作系统中,线程是小的调度单位)。在Linux系统中创建进程有两种方式:一是由操作系统创建,二是由父进程创建进程...
  • 文章目录1、多线程中使用fork存在隐患!1.1、考虑问题1.2、读者问题2、线程中fork 须慎用!3、推荐链接 1、多线程中使用fork存在隐患! 参考下图: 左侧------进程包含主线程,线程A,线程B三个线程以及一个全局...
  • Python多线程fork

    2017-11-21 18:45:19
    Python 多线程 fork
  • 关于线程的创建与使用,前几个博客说的很详细了,那么我们如果在某一个函数线程中调用fork函数有什么需要注意的吗? 一:线程中使用fork函数 在线程中调用 fork 函数,子进程只会启用调用 fork 函数的那条线程,...
  • 【Linux】利用fork()创建线程

    千次阅读 2016-12-19 17:35:37
    由于fork()结构的特殊性,如果要用fork()创建多个线程,并不像pthread_create()那样轻松,而且最关键的一点,是你利用for循环和fork()创建出来的多线程,会不明不白地多出N条线程。这主要是由于对fo
  • 浅谈在多线程下的fork的问题2. 死锁问题的模拟实现3. 解决办法 前言 有关进程、多线程fork的概念,请看我之前写的这两篇文章。 Linux:进程控制(进程创建、进程终止、进程等待、进程程序替换) Linux:详解多线程...
  • python并不支持真正意义上的多线程,Python中的多线程包,但是如果你想通过多线程提高代码的速度,使用多线程包并不是个好注意,Python中有一个GIL的东西,他会确保任何时候你的多个线程只有一个被执行,线程的执行...
  • fork()函数的调用会导致在子进程中除调用线程外的其它线程全都终止执行并消失,因此在多线程的情况下会导致死锁和内存泄露的情况。在进行多线程编程的时候尽量避免fork()的调用,同时在程序在进入main函数之前应避免...
  • fork 多线程程序带来的问题

    千次阅读 2011-12-26 11:10:21
    原文:http://blog.codingnow.com/2011/01/fork_multi_thread.html ...这两个步骤,序列化工作并没有独立在单独线程/进程里做,而是放在主线程的。IO 部分则在一个独立进程中。 序列化任务是个繁
  • 极不和谐的 fork 多线程程序 继续前几天的话题。做梦幻西游服务器优化的事情。以往的代码,定期存盘的工作分两个步骤,把 VM 里的动态数据序列化,然后把序列化后的数据写盘。这两个步骤,序列化工作并没有独立...
  • 1.JAVA多线程(十七)Java多线程ForkJoin框架 1.1 什么是ForkJoin框架?    Fork/Join 框架,可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。通过这种方式...
  • 多线程程序里不准使用fork :为什么??? UNIX上C++程序设计守则3 准则3:多线程程序里不准使用fork多线程程序里,在”自身以外的线程存在的状态”下一使用fork的话,就可能引起各种各样的问题.比较典型的例子就是,...
  • 多线程fork

    2014-04-10 15:34:30
    多线程程序与fork() 多线程程序里不准使用fork UNIX上C++程序设计守则3 准则3:多线程程序里不准使用fork多线程程序里,在”自身以外的线程存在的状态”下一使用fork的话,就可能引起各种各样的...
  • fork()和多线程

    千次阅读 2014-08-29 16:29:01
     在操作系统的基本概念中进程是程序的一次执行,且是拥有资源的最小单位和调度单位(在引入线程的操作系统中,线程是最小的调度单位)。在Linux系统中创建进程有两种方式:一是由操作系统创建,二是由父进程创建...
  • 1. 概述 java.util.concurrent.ForkJoinPool由Java大师Doug Lea主持编写,...本文中对Fork/Join框架的讲解,基于JDK1.8+中的Fork/Join框架实现,参考的Fork/Join框架主要源代码也基于JDK1.8+。 这几篇文章将试图解释
  • 当我们求解某些问题时,由于这些问题要处理的数据相当,或求解过程相当复杂,使得直接求解法在时间上相当长,或者根本无法直接求出。对于这类问题,我们往往先把它分解成几个子问题,找到求出这几个子问题的解法后...
  • skynet:fork

    2020-04-27 15:54:58
    skynet 是多线程框架,此外在 skynet 中,每个服务对应一个 lua 虚拟机,一个虚拟机上可以跑多个协程,但同一时刻只能有一个协程,每条消息处理由协程来完成。 在 skynet 中,线程创建方式: skynet.fork(func,…) ...
  • 在操作系统的基本概念中进程是程序的一次执行,且是拥有资源的最小单位和调度单位(在引入线程的操作系统中,线程是最小的调度单位)。在Linux系统中创建进程有两种方式:一是由操作系统创建,二是由父进程创建进程...
  • 进程在Linux系统下,使用os.fork(), 调用一次,返回两次,操作系统自动把当前进程(父进程)复制了一份(子进程),然后分别在父进程和子进程内返回。子进程永远返回0,父进程返回子进程的ID。经过这样做,父进程就能...
  • 不会,只会复制当前的线程 参考: https://www.cnblogs.com/liyuan989/p/4279210.html
  • linux线程fork

    2019-04-25 10:24:57
    线程调用fork函数时,就为子进程创建了整个进程地址空间的副本,子进程通过继承整个地址空间的副本,也会将父进程的互斥里、读写锁、条件变里的状态继承过来。也就是说,如果父进程中互斥里是锁着的,那么在子进程...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 81,791
精华内容 32,716
关键字:

fork多线程