前言
进程和线程,我在本科学习的时候就常常会听到大家谈论的时候说起来。我也很清楚一个进程可以由很多个线程。程序同样可以有很多个进程,等等巴拉巴拉一堆的。不过鉴于我本科的时候专心拉加权,无心代码,所以这么基础的玩意我一直都没有亲手实现过。。
好吧,现在终于到了彻底弄明白这些问题的时候了。并且,不仅仅是创立几个线程或者进程这么简单,我还要学习他们之间的通信方式。
第11章:进程和信号
首先第一个启动新进程的方式是通过system函数。
int system(const char *string)
sh -c string其实也就是启动了一个新的shell,字符串指针里放要执行的命令就行了。并且如果输入的命令不带有符号 &(即在后台运行),那么程序会在system调用这一行阻塞。因此这样的建立新进程的方式不太合适。
第二种方式
使用execl函数,该函数一共有6种变体
execl execlp execle execv execvp execve第一行是通过list参数来实现,所以末尾用使用 l, 第二种使用字符串数组来代替list参数。显得更加的简洁。不过需要自己建立参数字符串数组。如果要列出运行程序的路径,使用带p的,如果要加入环境参数,使用带e的函数。总之这个函数能够做到非常细致的配置。十分好用。
在程序中调用此函数时,原来的进程就会被替换掉了。所以execl函数后面的代码都不会继续运行了。
我在调用这个函数的时候我尝试给它加上&后运行的参数,但是程序报错,暂时没有找到别的方法,不过要实现这个功能可以通过system函数。
为了实现保留原有进程,并且开启一个新的进程,我们需要用到另一个函数了
fork()fork函数的返回值是pid_t型的变量, 因为在原进程和新的进程之中都会有这个返回值,而在原进程(或者说是父进程)中返回的是子进程的PID,子进程中返回0。这样我们就能做一个判断了。返回0的那个调用execl函数,启动想要的新进程。返回值为非零的进程继续执行原有的任务。
在父进程中使用wait()函数,父进程讲阻塞,知道子进程结束。wait返回值时子进程的PID
pid_t wait(int* stat_loc)stat_loc里的值是子进程的返回值或者exit函数的退出码。这样父进程就可以判断子进程的运行情况,然后再根据它做下一步处理。
如果子进程先结束,并且父进程还没有调用wait函数,或者父进程没有结束,那么子进程将会变成Z进程,即僵尸进程,它还存在于系统之中,退出码被保存起来了,等待父进程的调用。
使用waitpid()函数可以等待指定进程的结束
pid_t waitpid(pid_t pid, int* stat_loc, int options)option选项里的WNOHANG,可以防止wait函数阻塞,子进程未结束时就返回0,然后继续运行。如果waitpid失败就返回-1并且写入errno。
建立新进程的部分在这里就结束了,接下来是信号的学习。
在头文件signal.h之中,定义了SIG系列的信号,各自代表不同的值。并且其中SIGKILL与SIGSTOP是不能够被捕获或者忽略的。
在命令行中可以使用命令kill来发送信号
kill -HUB 512(pid_t)函数使用signal函数来定义信号的处理。该函数原型如下
void(*signal(int sig, void(*func)(int)))(int);最开始看着这个定义我的头都晕了, 这是个什么东西,好像是函数指针之类的东西但又不完全是。仔细百度研究了之后终于明白了。
首先,里面signal是一个函数,有两个参数,第一个时int型的信号名字,使用预定义的值,第二个参数是一个函数指针,接受带有一个int变量,返回值为void 的函数名字。这个signal函数的返回值同样是一个函数指针,这个函数指针指向一个带有一个int型参数,返回值是void的函数。简直醉了,这么复杂,好在平时使用感到时候不用管返回值,直接把signal当做普通函数调用就行了。比如:
signal(SIGINT, doit)同样,函数指针那个参数可以使用两个特殊的宏定义,SIG_IGN代表忽略该信号,SIG_DFL代表恢复默认行为。
除了使用命令行在终端发送信号之外,还可以通过kill函数发送,调用很简单,要发给谁,发什么
int kill(pid_t pid, int sig)
发送失败的话返回-1。
pause()函数,挂起程序,直到有一个信号来临。才继续运行。
使用sigaction函数更加好用,可以设置mask,flags,并且可以保持之前设置的参数。
第十二章:POSIX线程
完成了进程的学习之后,顺势来到了线程。在学习之前我已经知道线程就是同一个进程启动的,它们同时在程序里运行。使用线程的好处在于,创建一个新的线程代价比进程小得多。并且它们之间进行协同的处理相对于进程来说也方便很多。
编译线程的程序,需要在编译时添加
-lpthread库
并且需要许多函数需要可重入的版本,需要定义宏
_REENTRANT // -D_REENTRANT
现在开始学习如何创立新的线程,使用函数:
Int pthread_create(pthread_t*thread, pthread_attr_t *attr, void *(*start_routine)(void*), void *arg)
该函数的第一个参数是线程的标志,使用该标志来引用线程,第二个参数是属性,一般不需要设置,使用NULL即可,最后一个是函数指针。开始运行该指针指向的函数。pthread_create调用成功返回0,失败返回错误代码。
被调用的线程结束之后调用
void pthread_exit(void* retval)
退出函数。返回一个void*型的参数,注意不能指向局部变量,不然引起内存泄露。在主线程中,调用:
Int pthread_join(pthread_t th,void **thread_return);
来接收结束线程的返回参数。成功返回0,失败返回错误码。但是使用该函数时,程序将在此阻塞,直到等待的线程结束,收到返回值。
使用线程时,如果pthread_attr_t未设置,那么线程的默认状态是joinable.在线程结束之后它的资源不会被操作系统回收,一定需要调用pthread_join,如果想使用非阻塞的方式结束线程。那么可以使用两种方式:
1. 在线程开始时加入代码:
Pthread_detach(pthread_self());
2. 在父线程中调用
pthread_detach(thread_id);
这样就可以非阻塞的方式结束线程。
在几个线程同时运行的时候,有时候需要几个线程相互同步,这时候就可以使用信号量进行同步。信号量头文件是<semaphore.h>
int sem_init(sem_t *sem, int pshared,unsigned int value);
int sem_wait(sem* sem);
int sem_post(sem *sem);
int sem_destroy(sem_t *sem);
通过这四个函数进行初始化,增加,减少,清理信号量,就可以在线程中时间同步了。
需要做到互斥的时候,使用互斥量。使用的函数也非常相似:
pthread_mutex_init
pthread_mutex_lock
pthread_mutex_unlock
pthread_mutex_destory
使用这4个函数就可以使用互斥量了。
在创建线程的时候,如果传递给线程的参数是经常变化的,那么最好传递的时候,传递参数本身的值而不是它的地址。不然可能当线程创立读取参数的值的时候,该地址上的值已经变化了。
第13章:进程间的通信:管道
我以前一直不懂的问题就是进程之间如何进行通信,我知道可以使用socket进行网络通信,但是我觉得在本地上应该是不需要使用这么麻烦的方法的。学了这一章总算是知道一个方法了,可能还有其他的方法,等我以后再慢慢学。
通过使用
FILE * popen(const char *command, constchar* open_mode);
Int pclose(FILE* stream_to_close);
来启动一个linux系统的命令,再给这个命令输入或者读取这个命令的输出数据。
这个方式会启动一个新的shell,在传递的字符串里可以直接写shell命令,并且可以使用正则表达式,因为该命令是shell来进行分析的。
第二种方式,使用底层的pipe函数
Int pipe(int file_descriptor[2])
使用含有两个int值的数组,然后可以从file_descriptor[1]写入数据,从file_descriptor[0]读取出来,写入读取使用write与read函数。
通过pipe方法在父进程与子进程之间通信的时候,由于使用fork()函数时,程序复制了一份父进程中的数据,所以现在管道有了两个写入口,两个写出口。如果使用read读取时,需要把所有的写入口关闭,read函数才不会阻塞。
除了这样的方式以外,还可以建立管道文件来通信,使用管道文件之后就可以使用底层的open函数打开文件。更易于操作管道。但是一个管道文件只能用来做一个方向的通信,所以如果需要两个方向的通信最好还是使用两个管道文件。
建立管道文件:
Int mkfifo(const char *filename, mode_tmode);
Mode里面放文件的读写权限,一般写0777。
打开fifo文件的方式有两种一种是带NONBLOCK一种是不带的。
Open(const char*path, O_RDONLY)
Open(const char*path, O_WRONLY)
这种调用方式,如果读和写没能都打开的时候都会被阻塞,直到达到了配对,才会继续运行下去。带上NONBLOCK之后;
Open(const char*path, O_RDONLY | NONBLOCK)
Open(const char*path, O_WRONLY | NONBLOCK)
读的会直接跳过继续运行,打开成功。写的打开如果没有已经打开的RDONLY的话会打开失败,返回-1。
如果使用服务器客户端,使用fifo通信,可以使用mkfifo创建,服务器以RDONLY打开,然后等待客户端以WRONLY模式打开fifo文件。完成通信之后使用unlink删除创建的fifo文件就行。
然后这本书又重写了一次cd数据库应用程序,使用的不是之前写好的mysql访问函数让我有点伤心,使用的是最基础的DBM数据库。不过我读了一遍之后发现书上的做法简直是太棒了。不修改原有的代码,完美的隔离了服务器端与客户端。
这个做法让和C++中的封装的思想是完全一致的。给客户提供库函数,但是不告诉他们是如何实现的。所以在这里把原来的客户端代码当做用户,然后他调用的程序都是封装好的代码。我们在后面给重新使用fifo通信实现了服务器与客户端的分离。在客户端看来什么也没有变。并且这段代码手敲完之后,居然没有bug一遍直接通过了。看来我的练习效果真的还是不错的。