进程、线程、协程

进程

进程是系统进行资源分配和资源调度的基本单位.
进程有独立的资源.
不同进程通过进程间通信来通信.
进程的上下文切换开销大.
进程状态转移图
进程状态图
新进程由父进程fork而来,fork结束后并不立即复制父进程的内容,等真正使用的时候再复制,即写时拷贝(copy-on-write COW).为了效率,子进程一般会先父进程一步进行调用.
孤儿进程:父进程先退出,子进程还没退出,那么子进程将被托孤给init进程,此时的子进程就是孤儿进程.
僵死进程:在每个进程退出的时候,内核释放该进程所有的资源(包括打开的文件、占用的内存等),但是仍然为其保留一定的信息(包括进程号,退出状态,运行时间等),直到父进程通过外wait/waitpid来取时才释放.此时该进程处于僵死状态.
守护进程:守护进程是后台常驻内存的一种特殊进程,不和任何终端关联.守护进程是一个孤儿进程.守护进程的标准输入输出和错误输出都会被丢到/dev/null中.守护进程一般用作服务器进程.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/types.h>
#include <unistd.h>
//获取进程自身PID
pid_t getpid(void);
//获取进程自身父进程的ID PPID
pid_t getppid(void);
//创建一个子进程
//返回值有两次 1.子进程返回0 2.父进程返回子进程的PID
pid_t fork(void);
//创建一个子进程,但子进程共享父进程的空间.返回值有两次,子进程返回0,父进程返回子进程的进程号.vfork创建子进程后,父进程阻塞,直到子进程执行exec()或exit().
//vfork最初因为fork没有COW机制,而很多情况下fork后会紧接着执行exec,而exec的执行相当于之前fork复制的空间做了无用功,所以设计了vfork.
//fork有了COW机制后,唯一的代价仅仅是复制父进程页表的代价,所以vfork渐渐被废弃
pid_t vfork(void);

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

进程的销毁

  1. 主动
    1. main函数的自然返回 注意:main返回return不是结束,只是main函数结束,main函数结束后还需要调用全局变量、静态局部变量和main函数的局部变量的析构函数.
    2. 调用exit函数,标准函数 退出时会检查文件的打开情况,把文件缓冲区内容写回文件
    3. 调用_exit函数,系统调用函数 不会检测文件打开情况,直接退出
    4. 调用abort函数,产生SIGABRT信号
  2. 被动
    1. 收到信号

vfork创建的子进程中在main函数中return会导致进程异常结束,而调用exit则不会正是由于子进程和父进程公用栈空间,子进程return后导致栈空间被释放,而exit不会释放栈空间.

进程替换

1
2
3
4
5
6
7
8
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

进程间通信

进程间通信方式:

  1. 共享内存
  2. 消息队列
  3. 信号量
  4. 有名管道
  5. 无名管道
  6. 信号
  7. socket
  8. 文件

守护进程的创建

Linux有一个daemon函数用来创建守护进程.

1
2
3
4
#include <unistd.h>
int daemon(int nochdir, int noclose);
nochdir 如果为0,将当前工作目录切换到根目录/,否则工作目录不改变
noclose 如果为0,将0,1,2重定向到/dev/null,否则不变

守护进程的创建过程

  1. 屏蔽控制终端操作信号
  2. 处理SIGCHLD信号
  3. 后台运行(调用fork函数,然后让父进程退出运行,子进程继续在后台运行)
  4. 脱离控制终端,登陆会话和进程组setsid()
  5. 禁止会话重新打开控制终端(子进程调用fork函数,然后让子进程退出运行孙进程继续在后台运行)
  6. 重设文件创建掩码umask(0)
  7. 关闭打开的文件描述符
  8. 改变当前工作目录chdir("/")
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
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <signal.h>

void daemon()
{
//屏蔽控制终端操作信号
signal(SIGTTOU, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
signal(SIGTSTP, SIG_IGN);
signal(SIGHUP, SIG_IGN);

signal(SIGCHLD, SIG_IGN); //忽略子进程结束信号,避免僵死进程产生
pid_t pid = fork(); //创建子进程
if(-1 == pid)
{
exit(-1);
}
if(0 != pid)
{
std::cout << __LINE__ << "父进程:" << getpid() << "退出" << std::endl;
std::fflush(stdout);
exit(0); //父进程退出,子进程继续运行
}
setsid(); //子进程脱离控制终端,登陆会话和进程组,成为无终端的会话组长
pid = fork(); //创建孙进程
if(-1 == pid)
{
exit(-1);
}
if(0 != pid)
{
std::cout << __LINE__ << "子进程:" << getpid() << "退出" << std::endl;
std::fflush(stdout);
exit(0); //让子进程退出,孙进程继续运行,孙进程不再是会话组长,禁止程序重新打开控制终端
}
std::cout << __LINE__ << "孙进程:" << getpid() << "继续运行" << " PPID:" << getppid() << std::endl;
std::fflush(stdout);
chdir("/"); //改变工作目录
umask(0); //重设文件创建掩码
for(unsigned i=0; i<3; ++i)
{
close(i); //关闭标准输入,标准输出,标准错误输出
}
std::cout << __LINE__ << "关闭标准输入,标准输出,标准错误输出" << std::endl;
std::fflush(stdout);
return;
}

线程(轻量级进程)

线程是CPU调度和分派的基本单位.
一个进程中可以有多个线程(最少一个线程).它们共享代码空间和数据空间(全局变量和静态变量),文件描述符,信号,malloc分配的内存.
每个线程有自己独立的栈空间和程序计数器.
同进程内线程上下文切换快,资源消耗少.
线程的实现调用clone系统调用.

线程相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<pthread.h>
//线程的创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
//等待线程结束 返回等待线程的返回值
int pthread_join(pthread_t thread, void **retval);
//终止线程
void pthread_exit(void *retval);
//获取线程自身id
pthread_t pthread_self(void);
//将指定线程转变为detach状态
//1. 线程缺省情况下是joinable,其线程ID和退出状态将留给另一个线程对它调用pthread_join
//2. detach线程在终止时,所有资源将自动释放
int pthread_detach(pthread_t thread);

linux下获取线程Id有两种方式

  1. gettid
  2. pthread_self
    gettid获取的是内核中线程Id,pthread_self获取的是posix描述的线程Id

对于单线程的进程,内核中tid和pid相等,对于多线程的进程,它们的pid相等,tid则不相同.tid用于描述内核中真实的pid和tid信息.
pthread_self返回的是posix定义的线程Id,只是用来区分某个进程中不同的线程,当一个线程退出后,新创建的线程可以复用原来的id.pthread_self返回的线程控制块TCB首地址相对于进程数据段的偏移(pthread_create也是返回该值),只是用来描述同一进程中的不同线程.

线程创建函数调用图
线程状态图

线程间通信和同步方式

  1. 共享内存
  2. 消息队列
  3. 信号量
  4. 有名管道
  5. 无名管道
  6. socket
  7. 文件
  8. 互斥量
  9. 自旋锁
  10. 条件变量
  11. 读写锁
  12. 线程信号
  13. 全局变量

协程(微线程)

协程是用户态的轻量级线程.
协程调度完全由用户控制,上下文切换比线程快,占用资源少.
协程不需要多线程的锁机制,执行效率比多线程高.

进程学习代码

代码Git地址

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
#include <iostream>
#include <string>
#include <unistd.h>

using namespace std;

int main()
{
cout << "start fork" << " PID:" << getpid() << endl;
pid_t iPid = fork();
if(iPid < 0)
{
cout << "fork error" << " PID:" << getpid() << endl;
}
else if (iPid == 0)
{
cout << "child process" << " PID:" << getpid() << " PPID:" << getppid() << endl;
}
else
{
cout << "parent process" << " PID:" << getpid() << endl;
}

sleep(300);

return 0;
}

运行情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ./process_study
start fork PID:12128
parent process PID:12128
child process PID:12129 PPID:12128

$ pstree -p -a
... ...
├─sshd,724 -D
│ ├─sshd,609
│ │ └─sshd,611
│ │ └─bash,620
│ │ └─process_study,12128
│ │ └─process_study,12129
... ...

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
#include <iostream>
#include <string>
#include <unistd.h>

using namespace std;

int main()
{
cout << "start fork" << " PID:" << getpid() << endl;
for(int i=0; i<3; ++i)
{
pid_t iPid = fork();
if(iPid < 0)
{
cout << "fork error" << " PID:" << getpid() << endl;
}
else if (iPid == 0)
{
cout << "child process" << " PID:" << getpid() << " PPID:" << getppid() << endl;
}
else
{
cout << "parent process" << " PID:" << getpid() << endl;
}
}
sleep(300);

return 0;
}

运行情况

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
$ ./process_study 
start fork PID:16030
parent process PID:16030
parent process PID:16030
parent process PID:16030
child process PID:16033 PPID:16030
child process PID:16032 PPID:16030
parent process PID:16032
child process PID:16031 PPID:16030
parent process PID:16031
parent process PID:16031
child process PID:16034 PPID:16032
child process PID:16036 PPID:16031
child process PID:16035 PPID:16031
parent process PID:16035
child process PID:16037 PPID:16035

$ pstree -p -a
... ...
│ │ └─process_study,16030
│ │ ├─process_study,16031
│ │ │ ├─process_study,16035
│ │ │ │ └─process_study,16037
│ │ │ └─process_study,16036
│ │ ├─process_study,16032
│ │ │ └─process_study,16034
│ │ └─process_study,16033
... ...

分析流程

1
2
3
4
5
6
7
8
PID:16030 --fork--> PID:16030   --fork--> PID:16030 --fork--> PID:16030
PID:16033
PID:16032 --fork--> PID:16032
PID:16034
PID:16031 --fork--> PID:16031 --fork--> PID:16031
PID:16036
PID:16035 --fork--> PID:16035
PID:16037

线程学习代码

代码Git地址

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
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>

using namespace std;

void *Print(void *pArg)
{
int *pCount = (int*)pArg;
for(;*pCount<10;++(*pCount))
{
cout << "PID:" << getpid() << " TID:" << pthread_self() << " " << *pCount << endl;
sleep(1);
}
}

int main()
{
pthread_t t1;
pthread_t t2;

int iCount = 0;
if(pthread_create(&t1, NULL, Print, &iCount) == -1)
{
cout << "thread create error" << endl;
return 1;
}

if(pthread_create(&t2, NULL, Print, &iCount) == -1)
{
cout << "thread create error" << endl;
return 1;
}

pthread_join(t1, NULL);
pthread_join(t2, NULL);

return 0;
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
$ ./thread_study 
PID:31384 TID:140122741049088 0
PID:31384 TID:140122749441792 0
PID:31384 TID:140122741049088 1
PID:31384 TID:140122749441792 2
PID:31384 TID:140122741049088 3
PID:31384 TID:140122749441792 4
PID:31384 TID:140122741049088 5
PID:31384 TID:140122749441792 6
PID:31384 TID:140122741049088 7
PID:31384 TID:140122749441792 8
PID:31384 TID:140122741049088 9

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
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <boost/thread/thread.hpp>
#include <boost/chrono.hpp>
#include <boost/ref.hpp>

using namespace std;

void Print1(int *pCount)
{
for(;(*pCount)<10;++(*pCount))
{
cout << "PID:" << getpid() << " TID:" << pthread_self() << " " << *pCount << endl;
boost::this_thread::sleep_for(boost::chrono::seconds(1));
}
}

void Print2(int &count)
{
for(;count<10;++count)
{
cout << "PID:" << getpid() << " TID:" << pthread_self() << " " << count << endl;
boost::this_thread::sleep_for(boost::chrono::seconds(1));
}
}

int main()
{
int count = 1;
boost::thread t1(Print1, &count);
boost::thread t2(Print2, boost::ref(count));
t1.join();
t2.join();
return 0;
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
$ ./thread_study 
PID:11311 TID:140043269957376 1
PID:11311 TID:140043278350080 1
PID:11311 TID:140043269957376 2
PID:11311 TID:140043278350080 3
PID:11311 TID:140043269957376 4
PID:11311 TID:140043278350080 5
PID:11311 TID:140043269957376 6
PID:11311 TID:140043278350080 7
PID:11311 TID:140043269957376 8
PID:11311 TID:140043278350080 9

协程学习代码

C++协程库基于两种方案:

  1. 利用汇编代码控制协程的上下文切换 (libco, Boost.context)
  2. 利用操作系统提供的API来实现协程上下文切换

参考文档