内容概要
- 进程间通信
- 多线程编程
进程间通信
在Linux操作系统中,每个进程都是相互之间都是独立的,而如何实现进程之间的信息交流是很有必要的,这个被称为进程间通信(IPC, Interprocess Communication)。在Linux中,使用较多的进程间通信有如下几种:
- 管道(pipeline, 以及有名管道, FIFO)
- 信号通信 (signals)
- 消息队列 (message queue)
- 共享内存 (shared memory)
- 信号量 (semaphore)
- 套接字 (socket)
接下来,我就会记录前五种IPC的基本概念、操作和注意事项。
管道
首先,如果对于Linux shell比较熟悉的话,就会知道管道这个概念。比如
$ ps -ef | grep ntp
,这就是一个很典型的管道,因为管道两边其实是两个不同的进程,但是它们之间通过|
交换数据,而且需要注意,管道的运行并不是前面的程序运行结束,之后的程序才运行:两者是并行的,并且两个进程是父子关系。
在Linux的IPC中,管道相当于在内存中的一个特殊的文件,所以管道通信的操作其实是读写这个特殊的文件。
一般的操作是:
int fd[2];
pipe(fd);
...
close(fd[0]);
close(fd[1]);
但是仅仅有上面操作并没有什么用,而需要搭配fork()
,于是就可以进行父子进程之间的通信了。
int fd[2];
if(!fork()) {
close(fd[0]); //or close(fd[1])
...
write();
close(fd[1]);
} else {
close(fd[1]);
...
read();
close(fd[0]);
}
Notice: fd[0] 是读的file descriptor; fd[1]是写的file descriptor。另外,由于这个是无名管道,只限于这种拥有直接亲缘关系的进程之间进行进程通信。
有名管道
有名管道,FIFO (First In First Out),则可以突破这种限制,因为会创建一个真正的文件,进行进程间通信。
FIFO的创建有两种:
(mkfifo("fifo", O_CREAT | O_EXCL | 0666) < 0) && (errno != EEXIST)
or
mknod("fifo", S_IFIFO | 0666, 0)
接下来的就是对文件的读和写。不过FIFO是有一些特殊性的:
- 在blocking的情况下(default),写管道会一直阻塞到读管道打开!所以最好不要用non-blocking的方式打开写管道,会直接返回错误值。如果是父子进程的话,比较好的方法还是用信号量来保证打开的先后顺序,当然如果是不同的进程的话就无所谓。只要阻塞到读管道打开就好了;
- 另外,读写管道关闭后,反应略有差别:所有的写管道关闭后,读管道
read()
时,会得到0的返回值。但是所有的读管道关闭后,写管道就会收到一个信号SIGPIPE,因此可以用一个signal()
来捕捉这个信号进行处理。
信号通信
信号通信是Linux中一种非常特别的IPC,因为是在软件层面模拟中断的机制,可以直接进行user space和kernel space之间的交互。
首先对于一些信号的解释:
Signals | Description |
---|---|
SIGABRT | Process abort signal. |
SIGALRM | Alarm clock. |
SIGFPE | Erroneous arithmetic operation. |
SIGHUP | Hangup. (When the terminal was closed) |
SIGILL | Illegal instruction. |
SIGINT | Terminal interrupt signal. Ctrl+C |
SIGKILL | Kill (cannot be caught or ignored). |
SIGPIPE | Write on a pipe with no one to read it. |
SIGQUIT | Terminal quit signal. Ctrl+\ |
SIGSEGV | Invalid memory reference. |
SIGTERM | Termination signal. |
SIGUSR1 | User-defined signal 1. No predefined meaning |
SIGUSR2 | User-defined signal 2. No predefined meaning |
SIGCHLD | Child process terminated or stopped. |
SIGCONT | Continue executing, if stopped. |
SIGSTOP | Stop executing (cannot be caught or ignored, stop just means not executing but not killed, which can be signaled to continue with SIGCONT). |
SIGTSTP | Terminal stop signal. |
SIGTTIN | Background process attempting read. |
SIGTTOU | Background process attempting write. |
SIGBUS | Bus error. |
SIGPOLL | Pollable event. |
SIGPROF | Profiling timer expired. |
SIGSYS | Bad system call. |
SIGTRAP | Trace/breakpoint trap. |
SIGURG | High bandwidth data is available at a socket. |
SIGVTALRM | Virtual timer expired. |
SIGXCPU | CPU time limit exceeded. |
SIGXFSZ | File size limit exceeded. |
关于如何发送信号:
raise()
是可以给自己发送一个信号;
然而kill(pid_t pid, int sig)
则是给其它进程发送信号。
关于接收信号:
比较无脑的函数是void (*signal(int sig, void(*sa_handle)(int))(int)
,但是这个还是不鼓励使用。
比较合适的是sigaction(int sig, const struct sigaction *act, struct sigaction *oldact)
而其中的struct sigaction结构体的定义则如下:
struct sigaction {
void (*sa_handler) (int sig);
sigset_t sa_mask;
int sa_flags;
void (*sa_restore)(void);
}
以上的sa_handler皆可以是是SIG_DFL(系统默认操作),也可以是SIG_IGN(忽略信号,但是我们知道SIGKILL和SIGSTOP既不能被捕捉也不能被忽略),当然也可以是用户自定义函数。
使用一系列信号处理函数,就可以使用sigaction()
:
sigempty(&set), sigaddset(&set, SIG***), sigprocmask(,,), sigismember().
Notice:
signal()
和sigaction()
只是注册了一个信号处理函数,而当信号来到的时候,由于使用中断机制这个注册了的信号处理函数就可以发挥作用跳转到处理的函数那里,等处理完之后再回到原函数中。
共享内存
比如以上的读写文件来共享信息的方式,如果可以直接在内存中开一段进行共享会来的更加快捷和实在。共享内存 (shared memory) 就是这样一种IPC,而且也是其中最高效的一种。其实,内核专门就留了这样一段内存区,可供使用。进程可以将这段内存区映射到自己的私有地址空间,进程间只需要直接读取这一内存区就可以了。
操作步骤如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
/*
* create a segment in the memory
* for key_t key, general way of generating this parameter is using ftok("filename", any_int_flag) function;
* of course, using IPC_PRIVATE is also convenient, except for passing the shmid to other processes through pipe/fork/file...
*/
int shmget(key_t key, int size, int shmflag);
// map the segment to the address of the process
void *shmat(int shmid, const void* shmaddr, int shmflag);
...manipulate the pointer...
int shmdt(void *shmaddr); //detaches the segment
shmctl(shmid, IPC_RMID, NULL); // delete the segment
消息队列
消息队列(message queue),也是类似于FIFO的特性,不过还具有随机查询的功能,因为可以用队列ID进行标识和查询。
主要的函数有:
int msgget(key_t key, int flag);
/*Here the pointer can be pointed to any user-defined struct with the first member variable as long mtype;*/
int msgsnd(int msqid, const void* ptr, size_t size, int flag);
A practical tutorial: https://beej.us/guide/bgipc/output/html/multipage/mq.html
多线程编程
进程 VS 线程
在网上已经有非常多的文章描述关于进程和线程之间的区别。我只是稍微做一下总结:
- 进程之间在资源分配上是相互独立的,而线程只是进程的一个顺序代码流(execution flow)。同一个进程中的线程可以共享进程的资源,包括内存、文件描述符、信号等;
- Linux系统中, 从内核的角度看,其实每个线程都是被看做一个进程,所以没有进程和线程的差别,而线程也被称为light weight process;
- 进程具有自己独立的内存资源,但是线程却只有自己的执行堆栈和局部变量;
- 关于PID和TGID:每个线程的PID都是不同的,但是同一个线程组的TGID则是相同的,并且这个TGID是线程组长的PID;
- 所以相较于fork()(用来创建子进程),使用clone()(可以用来创建新线程)则比较省时省力;因为clone()可以决定哪些资源共享,哪些资源是拷贝;当然POSIX定义的Pthread也是用clone()来实现的。
关于进程创建的大致步骤:
include <pthread.h>
/*create a new thread and start executing from the function start_routine, with arg as parameters*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
void pthread_exit(void *retval); //exit the thread;
int pthread_join(pthread_t th, void **thread_return); //the main thread waits for the exiting of the created thread;
特别要注意线程之间是会互相影响的,以下四种情况就会导致进程的退出(from man page):
The new thread terminates in one of the following ways:
* It calls pthread_exit(3), specifying an exit status value that is available to another thread in the same process that calls pthread_join(3).
* It returns from start_routine(). This is equivalent to calling pthread_exit(3) with the value supplied in the return statement.
* It is canceled (see pthread_cancel(3)).
* Any of the threads in the process calls exit(3), or the main thread performs a return from main(). This causes the termination of all threads
in the process.
另外,可以在创建进程的时候指定相应的属性:绑定属性和分离属性。
其中绑定属性是决定,是否给用户线程固定分配一个内核线程(默认是不绑定的);
分离属性,则是决定该线程是否和主线程保持分离,如果分离的话,线程会马上释放并且结束,这可能导致pthread_create还在创建的时候,线程就结束了,从而导致错误。而且还可能导致,主线程不会等到子线程结束就结束了main()函数,从而使整个进程退出,当然子进程也退出了。这个错误特别要小心。
Notice: 由于pthread.h并非Linux下的默认库,所以编译时,需要加上
-lpthread
才能编译成功。