C++ Coder

HCP高性能计算架构,实现,编译器指令优化,算法优化, LLVM CLANG OpenCL CUDA OpenACC C++AMP OpenMP MPI

C++博客 首页 新随笔 联系 聚合 管理
  98 Posts :: 0 Stories :: 0 Comments :: 0 Trackbacks

#

http://blog.csdn.net/midgard/article/details/4084884

今天实在是头脑不听使唤了,没力气看书,写代码了。写点总结吧。
都说算法是内功,究竟练到什么程度才算修成了呢?
 
为什么要学习,强化算法?
首先强调的是,下面的原因均是建立在算法熟练到一定程度后的效果。不熟的话,未见得能达到效果。这些原因很现实。但很多程序员却不知道或不以为然。
  1. 比较世俗的方面,顶级软件公司笔试,面试会问到。别说你不想去谷歌,百度,微软,如果真的没想过,我希望你能想想自己是否具备进入的实力。如果还是没兴趣想,那就忘了这条,看看下面的原因吧。
  2. 如果算法熟练,能显著提高看代码的速度,越是工作久了,越会发现很多时候是在读别人写的代码,然后在其基础上追加功能,或修改bug。所以这是很现实的技能。
  3. 算法操练上手快,注意只是快,但不见得容易。因为练习省去了界面等操作,只利用编译器最基本的功能,非常适合初学者,编程环境不十分熟练的人来学习,别小看这一点,这能让你不论在家,在同学的电脑上,甚至在网吧,只要你想操练都是能迅速找到办法写程序实现的。
  4. 难学,有区分度,正因为难,属于内功级别的学习,不是工具型的学习,所以她经久不衰,这项技能能让你不论什么时候都能作为一个亮点技能展示,并运用。比如眼下学python的人不少,但如果再出现更进步,高效的语言,而且这是完全有可能的,又需要重新学习,那什么才是被各种纷繁语言覆盖的,能让你拥有后在短时间能适应任何语言层面的转换工作,更为通用的技能呢?无疑,数据结构+算法是其中一个,而且是非常重要的一项技能。而更为重要的是,算法这种技能,最实实在在的在于他的优势持久性,周围的人如果没有经过大量算法编码训练,只是看过书写过简单算法的人是很难短期内超越你的。也就是说,这项技能越高越能保证你的能力、水平在社会上具有唯一性。即:稀缺人才。
  5. 很多简单的UI操作,短期培训班出来的照着做也能做出来很炫的效果,要佩服微软的封装工作,把UI设计大大简化了。
  6. 对编程语言的基本功能会得到大量强化,比如操作符重载,数组,指针的运用,类的构建,对字符串的操作,以及STL,泛型的理解、运用。这里我想你应该看到了学习算法的过程是如何与计算机应用建立起了联系,因为你肯定在写程序时无数次与这些内容打交道,当然,不深入学习算法,也同样可以获得这些知识,而且.NET,Java,已经封装了非常多的这类基础操作,使得字符串处理变得容易多了。这就要看你的风格,需求了。这里我只是介绍深入学习算法能带来的好处,至于不深入学习有没有坏处,我不做评价。注意"深入"二字的修饰作用,如果没学过基本的数据结构,甚至数组、链表都没搞懂区别的话,那做程序员一定是不行的。迟早会遇到瓶颈,而且很快。
  7. 提供了足够的可扩展空间。对计算机科学的深层研究必备武器,比如操作系统,搜索引擎,编译器等等。要分析linux源代码,会发现,操作系统在内存管理,进程,线程管理,文件管理,协议栈中,运用了大量算法实现,如果基本算法都不理解,甚至不知道,很难说分析源代码能有多深入。因为看不懂啊。这时会有种遇到瓶颈的感觉。类似玩劲乐团的人,玩到一定程度,眼睛能跟上,但手速跟不上了。
  8. 注意,算法虽然是内功,但不是全部,还有很多知识是非常重要的,值得学习的,跟算法关系不太大的。比如设计模式,面向对象思想。所以算法学习到什么程度,自己酌情处理。毕竟越深入,会发现数学知识可能遇到瓶颈,但我想大多数人,只要不是搞科学研究,是不必深入到写龙格-库塔,FFT等算法的地步的。
  9. 总之深入学习算法的目的,对于程序员来说,是为了提高编程效率,提高解决问题的效率,能达到需要算法时,自己实现或找来能用的算法,很容易的嵌入到自己的程序中即可。
 
说说自己的学习,和强化路线吧,考虑到自己年事已高(28),相比之下算法,数据结构基础未见得比少数玩ACM的大一(18)学生强。
自信一点!如果真的多数大一学生比我强,那确实没法混了,转行的时候到了。呵呵。
当然不能妄自菲薄,要相信自己有这个学习能力,有这个潜质。相信自己的数学基础足够用,这点要感谢中国对中小学数学教育普及的深啊,
况且我也一直是数学方面的优等生。同时自己的编码,学习经验也将是事半功倍的最大帮助。
对内存的分析,编译器的使用,代码的细节,操作系统的运作原理等知识的理解都要好于大一学生。
虽然意识有点晚,但我看好这项技能,并希望拥有她,而且相信我可以通过努力实现目标。 
 
先定目标:
1 通过练习算法,提高分析问题,利用计算机解决问题的能力。
2 提高读代码,发现问题的能力。
3 提高对C++,C#等语言的运用能力,如STL的应用。C++各种语法细节的理解。
 
方法:练习+看书+做题+实战。
1. 练习常用数据结构,算法的实现,如,链表,队列,二叉树,排序,图等。以及下面说的第一阶段内容。
    要求代码实现,用C,C++,C#均可,还要能用STL实现。总之,越熟越好。能盲打最高境界了。到这个程度,算法内功对我来说,应付各种工作都足够用了。
2. 不断重新看数据结构,算法的讲解,从中更深刻的理解每种算法的核心思想。
3. 练习常用算法,看书,很容易过度训练,然后恶心。一个容易激发热情的方式是做题,ACM,topcoder等平台,都有良好的积分评级政策。还有就是笔试题。这个只在处于找工作期间有很大动力。
   没看到ACM题库中又多了一个绿色的对号,自己的blog又多了些人来访问,等级提高都会有一种潜在的成就感,荣誉感萌发。这些虚拟的等级就是源动力。
4. 写总结,1,2,3如果都不能激发你的潜在兴趣了,实在编恶心了,那就把感受,辛酸苦辣,收获写出来与大伙分享吧。同时也是对自己的过去进行回忆,发现自己的进步之处。
    不断给自己找出新的理由去继续学习。最重要的,这也是一个知识储备,构建自己的knowledge base的过程。对你未来的工作绝对是最宝贵,熟悉的财富。
    比如,万一你将来够牛了,想出书了,这些一点一滴的积累过程将是再好不过的素材了。
 
量化目标:
我一向认为能将质变效果量化了是一种很好的创新方式。因为他提供了一条完成的路。而且符合现代人,现代社会的价值观。
功成名就,无可厚非,这是一个大好的结果,如果办到,是千万老百姓都在寻找的答案。可惜每个成功人事的路都无法得到复制。
新东方老师曾问在做的学生,想GRE取得高分,拿奖学金么? 当然想。
答案:熟背2万英语单词,得2万美刀,背的过程中去想1个单词就是1刀,全部完成后才能兑现。
这个例子在我印象中很多年,一直是我认为将质变量化的最为典型,贴切的例子。
理想,梦想,高薪,富足生活,都可以看作一种质变,而努力奋斗的生活就是量化的过程。不同的是有人提前知道路在哪里,有人走着走着才知道。
有人虽然知道路,但怕黑怕孤独不愿走,有人坚持下去了,走到了最后;有人走着走着发现走错路了,有人走着走着庆幸的发现走对路了。
很难说,哪条路好走,哪条路是对的, 相比之下我更倾向于能看到结果的路,这可能就是老人说的,要有个奔头吧。
 
1 TopCoder上250的题目(包括读题),曾有个人说20分钟能搞定去Google不成问题。我试过些,包括读题还是比较有难度,所以现实点吧:30分钟内搞定。
2 做完500 ACM题目。
3 如果能做到1,2相信算法能力已经有小小质变了。再加个基础要求吧,对于常用算法能40分钟内在纸上完成代码。
 
另外转一个网上的评述:

一般要做到50行以内的程序不用调试、100行以内的二分钟内调试成功.acm主要是考算法的, 

 
主要时间是花在思考算法上,不是花在写程序与debug上。   
下面给个计划你练练:   
第一阶段:练经典常用算法,下面的每个算法给我打上十到二十遍,同时自己精简代码,因为太常用,所以要练到写时不用想,10-15分钟内打完,甚至关掉显示器都可以把程序打出来.   
1.最短路(Floyd、Dijstra,BellmanFord)   
2.最小生成树(先写个prim,kruscal要用并查集,不好写)   
3.大数(高精度)加减乘除   
4.二分查找. (代码可在五行以内)   
5.叉乘、判线段相交、然后写个凸包.   
6.BFS、DFS,同时熟练hash表(要熟,要灵活,代码要简)   
7.数学上的有:辗转相除(两行内),线段交点、多角形面积公式.   
8. 调用系统的qsort, 技巧很多,慢慢掌握。   
9. 任意进制间的转换 
 
第二阶段:练习复杂一点,但也较常用的算法。   
如:   
1. 二分图匹配(匈牙利),最小路径覆盖   
2. 网络流,最小费用流。   
3. 线段树.   
4. 并查集。   
5. 熟悉动态规划的各个典型:LCS、最长递增子串、三角剖分、记忆化dp   
6. 博弈类算法。博弈树,二进制法等。   
7. 最大团,最大独立集。   
8. 判断点在多边形内。   
9. 差分约束系统.   
10. 双向广度搜索、A*算法,最小耗散优先. 
时间:
人都是总结,展望容易,靠想靠说当然容易些,尤其是技术人员。真的去做,去完成却是最难的。所以上面说了一大堆,真正最后能实现多少,都是未知数。定些计划,往往能激发自己去完成,如果再把这个计划公开,那就更能加上点督促的力量。2-3年吧,是个比较合理的估计,那时工作也快满5年了。应该有一定积累了,也是质变的时候了。
posted @ 2013-01-08 14:56 jackdong 阅读(403) | 评论 (0)编辑 收藏



简单介绍下ACM,汗!还得现搜索一下。

ACM(Association for Computing Machinery)国际计算机组织

通常说的ACM是指 ACM国际大学生程序设计竞赛
基本可以看作是写各种算法的比赛。

 

国内常用的ACM练习站:北大的比较有人气。http://acm.pku.edu.cn/JudgeOnline/
http://poj.org/

刚刚注册了下,体验了一下流程。

1 先Register一个账户,需要提交些个人信息,没啥顾虑就写真名吧。

2 到Problems里面查阅题目。可以先按1000的提示练习下。

3 在Submit Problem中写入解题代码,以C++格式提交。比如针对题目1000的,C++代码是

#include <iostream>
using namespace std;

int main(int argc, const char** argv)
{
   int a, b;
   cin >>a >> b;
   cout << a+b <<endl;
   return 0;
}

 

4 然后界面回显示你提交信息的状态,网站会自动刷新。如果有错,可以查看Compile Error。

   直到显示蓝色的Accepted表示通过。

5 你可以在User 栏中选择自己的ID,查看信息,比如我刚解决一道题目后,会显示。

 

Rank: 50702 Solved Problems List
Solved: 1 1000
Submissions: 2
School: ×××
Email: ×××@××.com

 

6 应该解决问题越多,排名越高,积分越多吧。挺有趣的。 据说练习的人都很上瘾,尝试了下发现确实在线的人很多,以后再做做。

 

另外国际上也有个网站比较适合练习算法, topcoder, 择日介绍下。
http://blog.csdn.net/midgard/article/details/4073319

posted @ 2013-01-08 14:02 jackdong 阅读(812) | 评论 (0)编辑 收藏

http://linux.ccidnet.com/art/302/20061117/953467_1.html
管道及有名管道

在本系列序中作者概述了 linux 进程间通信的几种主要手段。其中管道和有名管道是最早的进程间通信机制之一,管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。 认清管道和有名管道的读写规则是在程序中应用它们的关键,本文在详细讨论了管道和有名管道的通信机制的基础上,用实例对其读写规则进行了程序验证,这样做有利于增强读者对读写规则的感性认识,同时也提供了应用范例。

1、 管道概述及相关API应用

1.1 管道相关的关键概念

管道是Linux支持的最初Unix IPC形式之一,具有以下特点:

管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;

只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);

单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。

数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

1.2管道的创建:

#include 
int pipe(int fd[2])

该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。

1.3管道的读写规则:

管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等。

从管道中读取数据:

如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.1要求PIPE_BUF至少为512字节,red hat 7.2中为4096)。

关于管道的读规则验证:

* readtest.c *
#include 
#include 
#include 
main()
{
int pipe_fd[2];
pid_t pid;
char r_buf[100];
char w_buf[4];
char* p_wbuf;
int r_num;
int cmd;

memset(r_buf,0,sizeof(r_buf));
memset(w_buf,0,sizeof(r_buf));
p_wbuf=w_buf;
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}

if((pid=fork())==0)
{
printf("\n");
close(pipe_fd[1]);
sleep(3);//确保父进程关闭写端
r_num=read(pipe_fd[0],r_buf,100);
printf( "read num is %d the data read from the pipe is %d\n",r_num,atoi(r_buf));

close(pipe_fd[0]);
exit();
}
else if(pid>0)
{
close(pipe_fd[0]);//read
strcpy(w_buf,"111");
if(write(pipe_fd[1],w_buf,4)!=-1)
printf("parent write over\n");
close(pipe_fd[1]);//write
printf("parent close fd[1] over\n");
sleep(10);
} 
}

程序输出结果:

* parent write over
* parent close fd[1] over
* read num is 4 the data read from the pipe is 111

附加结论:管道写端关闭后,写入的数据将一直存在,直到读出为止。

向管道中写入数据:

向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。

注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。对管道的写规则的验证1:写端对读端存在的依赖性

#include 
#include 
main()
{
int pipe_fd[2];
pid_t pid;
char r_buf[4];
char* w_buf;
int writenum;
int cmd;

memset(r_buf,0,sizeof(r_buf));
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}

if((pid=fork())==0)
{
close(pipe_fd[0]);
close(pipe_fd[1]);
sleep(10); 
exit();
}
else if(pid>0)
{
sleep(1); //等待子进程完成关闭读端的操作
close(pipe_fd[0]);//write
w_buf="111";
if((writenum=write(pipe_fd[1],w_buf,4))==-1)
printf("write to pipe error\n");
else 
printf("the bytes write to pipe is %d \n", writenum);

close(pipe_fd[1]);
} 
}

则输出结果为: Broken pipe,原因就是该管道以及它的所有fork()产物的读端都已经被关闭。如果在父进程中保留读端,即在写完pipe后,再关闭父进程的读端,也会正常写入pipe,读者可自己验证一下该结论。因此,在向管道写入数据时,至少应该存在某一个进程,其中管道读端没有被关闭,否则就会出现上述错误(管道断裂,进程收到了SIGPIPE信号,默认动作是进程终止)对管道的写规则的验证2:linux不保证写管道的原子性验证

#include 
#include 
#include 
main(int argc,char**argv)
{
int pipe_fd[2];
pid_t pid;
char r_buf[4096];
char w_buf[4096*2];
int writenum;
int rnum;
memset(r_buf,0,sizeof(r_buf)); 
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}

if((pid=fork())==0)
{
close(pipe_fd[1]);
while(1)
{
sleep(1); 
rnum=read(pipe_fd[0],r_buf,1000);
printf("child: readnum is %d\n",rnum);
}
close(pipe_fd[0]);

exit();
}
else if(pid>0)
{
close(pipe_fd[0]);//write
memset(r_buf,0,sizeof(r_buf)); 
if((writenum=write(pipe_fd[1],w_buf,1024))==-1)
printf("write to pipe error\n");
else 
printf("the bytes write to pipe is %d \n", writenum);
writenum=write(pipe_fd[1],w_buf,4096);
close(pipe_fd[1]);
} 
}

输出结果:

the bytes write to pipe 1000
the bytes write to pipe 1000 //注意,此行输出说明了写入的非原子性
the bytes write to pipe 1000
the bytes write to pipe 1000
the bytes write to pipe 1000
the bytes write to pipe 120 //注意,此行输出说明了写入的非原子性
the bytes write to pipe 0
the bytes write to pipe 0
......

结论:

写入数目小于4096时写入是非原子的!

如果把父进程中的两次写入字节数都改为5000,则很容易得出下面结论:

写入管道的数据量大于4096字节时,缓冲区的空闲空间将被写入数据(补齐),直到写完所有数据为止,如果没有进程读数据,则一直阻塞。


1.4管道应用实例:

实例一:用于shell

管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入。比如,当在某个shell程序(Bourne shell或C shell等)键入who│wc -l后,相应shell程序将创建who以及wc两个进程和这两个进程间的管道。考虑下面的命令行:

$kill -l 运行结果见附一。

$kill -l | grep SIGRTMIN 运行结果如下:

30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1
34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5
38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9
42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14

实例二:用于具有亲缘关系的进程间通信

下面例子给出了管道的具体应用,父进程通过管道发送一些命令给子进程,子进程解析命令,并根据命令作相应处理。

#include 
#include 
main()
{
int pipe_fd[2];
pid_t pid;
char r_buf[4];
char** w_buf[256];
int childexit=0;
int i;
int cmd;

memset(r_buf,0,sizeof(r_buf));

if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}
if((pid=fork())==0)
//子进程:解析从管道中获取的命令,并作相应的处理
{
printf("\n");
close(pipe_fd[1]);
sleep(2);

while(!childexit)
{ 
read(pipe_fd[0],r_buf,4);
cmd=atoi(r_buf);
if(cmd==0)
{
printf("child: receive command from parent over\n now child process exit\n");
childexit=1;
}

else if(handle_cmd(cmd)!=0)
return;
sleep(1);
}
close(pipe_fd[0]);
exit();
}
else if(pid>0)
//parent: send commands to child
{
close(pipe_fd[0]);

w_buf[0]="003";
w_buf[1]="005";
w_buf[2]="777";
w_buf[3]="000";
for(i=0;i<4;i++)
write(pipe_fd[1],w_buf,4);
close(pipe_fd[1]);
} 
}
//下面是子进程的命令处理函数(特定于应用):
int handle_cmd(int cmd)
{
if((cmd<0)||(cmd>256))
//suppose child only support 256 commands
{
printf("child: invalid command \n");
return -1;
}
printf("child: the cmd from parent is %d\n", cmd);
return 0;
}

1.5管道的局限性

管道的主要局限性正体现在它的特点上:

只支持单向数据流;

只能用于具有亲缘关系的进程之间;

没有名字;

管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);

管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等。

2、 有名管道概述及相关API应用

2.1 有名管道相关的关键概念

管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

2.2有名管道的创建

#include 
#include 
int mkfifo(const char * pathname, mode_t mode)

该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。

2.3有名管道的打开规则

有名管道比管道多了一个打开操作:open。

FIFO的打开规则:

如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。

如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。对打开规则的验证参见附2。


2.4有名管道的读写规则

从FIFO中读取数据:

约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。

如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。

对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。

读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。

如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。

注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。

向FIFO中写入数据:

约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。

对于设置了阻塞标志的写操作:

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。

对于没有设置阻塞标志的写操作:

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;

对FIFO读写规则的验证:

下面提供了两个对FIFO的读写程序,适当调节程序中的很少地方或者程序的命令行参数就可以对各种FIFO读写规则进行验证。

程序1:写FIFO的程序

#include 
#include 
#include 
#include 
#define FIFO_SERVER "/tmp/fifoserver"

main(int argc,char** argv)
//参数为即将写入的字节数
{
int fd;
char w_buf[4096*2];
int real_wnum;
memset(w_buf,0,4096*2);
if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
printf("cannot create fifoserver\n");

if(fd==-1)
if(errno==ENXIO)
printf("open error; no reading process\n");

fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);
//设置非阻塞标志
//fd=open(FIFO_SERVER,O_WRONLY,0);
//设置阻塞标志
real_wnum=write(fd,w_buf,2048);
if(real_wnum==-1)
{
if(errno==EAGAIN)
printf("write to fifo error; try later\n");
}
else 
printf("real write num is %d\n",real_wnum);
real_wnum=write(fd,w_buf,5000);
//5000用于测试写入字节大于4096时的非原子性
//real_wnum=write(fd,w_buf,4096);
//4096用于测试写入字节不大于4096时的原子性

if(real_wnum==-1)
if(errno==EAGAIN)
printf("try later\n");
}

程序2:与程序1一起测试写FIFO的规则,第一个命令行参数是请求从FIFO读出的字节数

#include 
#include 
#include 
#include 
#define FIFO_SERVER "/tmp/fifoserver"

main(int argc,char** argv)
{
char r_buf[4096*2];
int fd;
int r_size;
int ret_size;
r_size=atoi(argv[1]);
printf("requred real read bytes %d\n",r_size);
memset(r_buf,0,sizeof(r_buf));
fd=open(FIFO_SERVER,O_RDONLY|O_NONBLOCK,0);
//fd=open(FIFO_SERVER,O_RDONLY,0);
//在此处可以把读程序编译成两个不同版本:阻塞版本及非阻塞版本
if(fd==-1)
{
printf("open %s for read error\n");
exit(); 
}
while(1)
{

memset(r_buf,0,sizeof(r_buf));
ret_size=read(fd,r_buf,r_size);
if(ret_size==-1)
if(errno==EAGAIN)
printf("no data avlaible\n");
printf("real read bytes %d\n",ret_size);
sleep(1);
} 
pause();
unlink(FIFO_SERVER);
}

程序应用说明:

把读程序编译成两个不同版本:

阻塞读版本:br

以及非阻塞读版本nbr

把写程序编译成两个四个版本:

非阻塞且请求写的字节数大于PIPE_BUF版本:nbwg

非阻塞且请求写的字节数不大于PIPE_BUF版本:版本nbw

阻塞且请求写的字节数大于PIPE_BUF版本:bwg

阻塞且请求写的字节数不大于PIPE_BUF版本:版本bw

下面将使用br、nbr、w代替相应程序中的阻塞读、非阻塞读

验证阻塞写操作:

当请求写入的数据量大于PIPE_BUF时的非原子性:

nbr 1000 
bwg

当请求写入的数据量不大于PIPE_BUF时的原子性:

nbr 1000 
bw

验证非阻塞写操作:

当请求写入的数据量大于PIPE_BUF时的非原子性:

nbr 1000 
nbwg

请求写入的数据量不大于PIPE_BUF时的原子性:

nbr 1000 
nbw

不管写打开的阻塞标志是否设置,在请求写入的字节数大于4096时,都不保证写入的原子性。但二者有本质区别:

对于阻塞写来说,写操作在写满FIFO的空闲区域后,会一直等待,直到写完所有数据为止,请求写入的数据最终都会写入FIFO;

而非阻塞写则在写满FIFO的空闲区域后,就返回(实际写入的字节数),所以有些数据最终不能够写入。

对于读操作的验证则比较简单,不再讨论。

2.5有名管道应用实例

在验证了相应的读写规则后,应用实例似乎就没有必要了。

小结:

管道常用于两个方面:(1)在shell中时常会用到管道(作为输入输入的重定向),在这种应用方式下,管道的创建对于用户来说是透明的;(2)用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。

FIFO可以说是管道的推广,克服了管道无名字的限制,使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信。

管道和FIFO的数据是字节流,应用程序之间必须事先确定特定的传输"协议",采用传播具有特定意义的消息。

要灵活应用管道及FIFO,理解它们的读写规则是关键。

附1:kill -l 的运行结果,显示了当前系统支持的所有信号:

1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1
34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5
38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9
42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14
50) SIGRTMAX-13 51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-10
54) SIGRTMAX-9 55) SIGRTMAX-8 56) SIGRTMAX-7 57) SIGRTMAX-6
58) SIGRTMAX-5 59) SIGRTMAX-4 60) SIGRTMAX-3 61) SIGRTMAX-2
62) SIGRTMAX-1 63) SIGRTMAX

除了在此处用来说明管道应用外,接下来的专题还要对这些信号分类讨论。

附2:对FIFO打开规则的验证(主要验证写打开对读打开的依赖性)

#include 
#include 
#include 
#include 
#define FIFO_SERVER "/tmp/fifoserver"

int handle_client(char*);
main(int argc,char** argv)
{
int r_rd;
int w_fd;
pid_t pid;

if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
printf("cannot create fifoserver\n");
handle_client(FIFO_SERVER);

}

int handle_client(char* arg)
{
int ret;
ret=w_open(arg);
switch(ret)
{
case 0:
{ 
printf("open %s error\n",arg);
printf("no process has the fifo open for reading\n");
return -1;
}
case -1:
{
printf("something wrong with open the fifo except for ENXIO");
return -1;
}
case 1:
{
printf("open server ok\n");
return 1;
}
default:
{
printf("w_no_r return ????\n");
return 0;
}
} 
unlink(FIFO_SERVER);
}

int w_open(char*arg)
//0 open error for no reading
//-1 open error for other reasons
//1 open ok
{
if(open(arg,O_WRONLY|O_NONBLOCK,0)==-1)
{ if(errno==ENXIO)
{
return 0;
}
else
return -1;
}
return 1;

}

参考文献:

UNIX网络编程第二卷:进程间通信,作者:W.Richard Stevens,译者:杨继张,清华大学出版社。丰富的UNIX进程间通信实例及分析,对Linux环境下的程序开发有极大的启发意义。

linux内核源代码情景分析(上、下),毛德操、胡希明著,浙江大学出版社,当要验证某个结论、想法时,最好的参考资料;

UNIX环境高级编程,作者:W.Richard Stevens,译者:尤晋元等,机械工业出版社。具有丰富的编程实例,以及关键函数伴随Unix的发展历程。

http://www.linux.org.tw/CLDP/gb/Secure-Programs-HOWTO/x346.html 点明linux下sigaction的实现基础,linux源码../kernel/signal.c更说明了问题;

pipe手册,最直接而可靠的参考资料

fifo手册,最直接而可靠的参考资料

关于作者

郑彦兴,男,现攻读国防科大计算机学院网络方向博士学位。您可以通过电子邮件 mlinux@163.com和他联系。

posted @ 2013-01-07 11:33 jackdong 阅读(932) | 评论 (0)编辑 收藏

http://www.pcdog.com/edu/linux/13/11/y237287.html
引言

    线程(thread)技术早在60年代就被提出,但真正应用多线程操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多

    为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。

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

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

    除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

    1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
    2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
    3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

    下面我们先来尝试编写一个简单的多线程程序。

简单的多线程编程

    Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面我们展示一个最简单的多线程程序example1.c。

 

/* example.c*/ #include <stdio.h> #include <pthread.h> void thread(void) { int i; for(i=0;i<3;i++) printf("This is a pthread.\n"); } int main(void) { pthread_t id; int i,ret; ret=pthread_create(&id,NULL,(void *) thread,NULL); if(ret!=0){ printf ("Create pthread error!\n"); exit (1); } for(i=0;i<3;i++) printf("This is the main process.\n"); pthread_join(id,NULL); return (0); }

 

    我们编译此程序:
    gcc example1.c -lpthread -o example1
    运行example1,我们得到如下结果:
    This is the main process.
    This is a pthread.
    This is the main process.
    This is the main process.
    This is a pthread.
    This is a pthread.
    再次运行,我们可能得到如下结果:
    This is a pthread.
    This is the main process.
    This is a pthread.
    This is the main process.
    This is a pthread.
    This is the main process.

    前后两次结果不一样,这是两个线程争夺CPU资源的结果。上面的示例中,我们使用到了两个函数,  pthread_create和pthread_join,并声明了一个pthread_t型的变量。
    pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义:
    typedef unsigned long int pthread_t;
    它是一个线程的标识符。函数pthread_create用来创建一个线程,它的原型为:
    extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
    void *(*__start_routine) (void *), void *__arg));
    第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。对线程属性的设定和修改我们将在下一节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。

    函数pthread_join用来等待一个线程的结束。函数原型为:
    extern int pthread_join __P ((pthread_t __th, void **__thread_return));

    第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。它的函数原型为:
  extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));

    唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。

    在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数pthread_create,pthread_join和pthread_exit。下面,我们来了解线程的一些常用属性以及如何设置这些属性。

 

修改线程的属性

    在上一节的例子里,我们用pthread_create函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参数设为NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。

    属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义,喜欢追根问底的人可以自己去查看。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。

    关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

    设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。

#include <pthread.h> pthread_attr_t attr; pthread_t tid; /*初始化属性值,均设为默认值*/ pthread_attr_init(&attr); pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); pthread_create(&tid, &attr, (void *) my_function, NULL);

    线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

    另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。

#include <pthread.h> #include <sched.h> pthread_attr_t attr; pthread_t tid; sched_param param; int newprio=20; pthread_attr_init(&attr); pthread_attr_getschedparam(&attr, &param); param.sched_priority=newprio; pthread_attr_setschedparam(&attr, &param); pthread_create(&tid, &attr, (void *)myfunction, myarg);

线程的数据处理

    和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。

    4.1 线程数据

    在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。

    和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。
    创建键的函数原型为:
    extern int pthread_key_create __P ((pthread_key_t *__key,
    void (*__destr_function) (void *)));

    第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它执行这个函数,以后的调用将被它忽略。

    在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。

/* 声明一个键*/ pthread_key_t myWinKey; /* 函数 createWindow */ void createWindow ( void ) { Fl_Window * win; static pthread_once_t once= PTHREAD_ONCE_INIT; /* 调用函数createMyKey,创建键*/ pthread_once ( & once, createMyKey) ; /*win指向一个新建立的窗口*/ win=new Fl_Window( 0, 0, 100, 100, "MyWindow"); /* 对此窗口作一些可能的设置工作,如大小、位置、名称等*/ setWindow(win); /* 将窗口指针值绑定在键myWinKey上*/ pthread_setpecific ( myWinKey, win); } /* 函数 createMyKey,创建一个键,并指定了destructor */ void createMyKey ( void ) { pthread_keycreate(&myWinKey, freeWinKey); } /* 函数 freeWinKey,释放空间*/ void freeWinKey ( Fl_Window * win){ delete win; }

    这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数pthread_getspecific得到。在上面的例子中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下:
    extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
    extern void *pthread_getspecific __P ((pthread_key_t __key));

    这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线程数据的释放必须在释放键之前完成。


  4.2 互斥锁

    互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。

    我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。

void reader_function ( void ); void writer_function ( void ); char buffer; int buffer_has_item=0; pthread_mutex_t mutex; struct timespec delay; void main ( void ){ pthread_t reader; /* 定义延迟时间*/ delay.tv_sec = 2; delay.tv_nec = 0; /* 用默认属性初始化一个互斥锁对象*/ pthread_mutex_init (&mutex,NULL); pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL); writer_function( ); } void writer_function (void){ while(1){ /* 锁定互斥锁*/ pthread_mutex_lock (&mutex); if (buffer_has_item==0){ buffer=make_new_item( ); buffer_has_item=1; } /* 打开互斥锁*/ pthread_mutex_unlock(&mutex); pthread_delay_np(&delay); } } void reader_function(void){ while(1){ pthread_mutex_lock(&mutex); if(buffer_has_item==1){ consume_item(buffer); buffer_has_item=0; } pthread_mutex_unlock(&mutex); pthread_delay_np(&delay); } }

    这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,一般情况下,选用最后一个默认属性。

    pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。

    上面的例子非常简单,就不再介绍了,需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。


4.3 条件变量

    前一节中我们讲述了如何使用互斥锁来实现线程间数据的共享和通信,互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。

    条件变量的结构为pthread_cond_t,函数pthread_cond_init()被用来初始化一个条件变量。它的原型为:
    extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));

    其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_ destroy(pthread_cond_t cond)。 

    函数pthread_cond_wait()使线程阻塞在一个条件变量上。它的函数原型为:
    extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
    pthread_mutex_t *__mutex));

    线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。

    另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为:
    extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
    pthread_mutex_t *__mutex, __const struct timespec *__abstime));

    它比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。
    函数pthread_cond_signal()的原型为:
    extern int pthread_cond_signal __P ((pthread_cond_t *__cond));

    它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待。下面是使用函数pthread_cond_wait()和函数pthread_cond_signal()的一个简单的例子。

pthread_mutex_t count_lock; pthread_cond_t count_nonzero; unsigned count; decrement_count () { pthread_mutex_lock (&count_lock); while(count==0) pthread_cond_wait( &count_nonzero, &count_lock); count=count -1; pthread_mutex_unlock (&count_lock); } increment_count(){ pthread_mutex_lock(&count_lock); if(count==0) pthread_cond_signal(&count_nonzero); count=count+1; pthread_mutex_unlock(&count_lock); }   

    count值为0时,decrement函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock。此时,当调用到函数increment_count时,pthread_cond_signal()函数改变条件变量,告知decrement_count()停止阻塞。读者可以试着让两个线程分别运行这两个函数,看看会出现什么样的结果。

    函数pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。



 4.4 信号量

    信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件/usr/include/semaphore.h中定义。

    信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
    extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));

    sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。

    函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。
    函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
    函数sem_destroy(sem_t *sem)用来释放信号量sem。

    下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。

/* File sem.c */ #include <stdio.h> #include <pthread.h> #include <semaphore.h> #define MAXSTACK 100 int stack[MAXSTACK][2]; int size=0; sem_t sem; /* 从文件1.dat读取数据,每读一次,信号量加一*/ void ReadData1(void){ FILE *fp=fopen("1.dat","r"); while(!feof(fp)){ fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]); sem_post(&sem); ++size; } fclose(fp); } /*从文件2.dat读取数据*/ void ReadData2(void){ FILE *fp=fopen("2.dat","r"); while(!feof(fp)){ fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]); sem_post(&sem); ++size; } fclose(fp); } /*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/ void HandleData1(void){ while(1){ sem_wait(&sem); printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1], stack[size][0]+stack[size][1]); --size; } } void HandleData2(void){ while(1){ sem_wait(&sem); printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1], stack[size][0]*stack[size][1]); --size; } } int main(void){ pthread_t t1,t2,t3,t4; sem_init(&sem,0,0); pthread_create(&t1,NULL,(void *)HandleData1,NULL); pthread_create(&t2,NULL,(void *)HandleData2,NULL); pthread_create(&t3,NULL,(void *)ReadData1,NULL); pthread_create(&t4,NULL,(void *)ReadData2,NULL); /* 防止程序过早退出,让它在此无限期等待*/ pthread_join(t1,NULL); }

    在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。 我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果:
    Multiply:-1*-2=2
    Plus:-1+-2=-3
    Multiply:9*10=90
    Plus:-9+-10=-19
    Multiply:-7*-8=56
    Plus:-5+-6=-11
    Multiply:-3*-4=12
    Plus:9+10=19
    Plus:7+8=15
    Plus:5+6=11

    从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。

    小结 

    多线程编程是一个很有意思也很有用的技术,使用多线程技术的网络蚂蚁是目前最常用的下载工具之一,使用多线程技术的grep比单线程的grep要快上几倍,类似的例子还有很多。希望大家能用多线程技术写出高效实用的好程序来。

posted @ 2013-01-07 11:24 jackdong 阅读(346) | 评论 (0)编辑 收藏

http://www.pcdog.com/edu/linux/13/11/y237288.html

管道技术是Linux的一种基本的进程间通信技术。在本文中,我们将为读者介绍管道技术的模型,匿名管道和命名管道技术的定义和区别,以及这两种管道的创建方法。同时,阐述如何在应用程序和命令行中通过管道进行通信的详细方法。

    一、管道技术模型 

    管道技术是Linux操作系统中历来已久的一种进程间通信机制。所有的管道技术,无论是半双工的匿名管道,还是命名管道,它们都是利用FIFO排队模型来指挥进程间的通信。对于管道,我们可以形象地把它们当作是连接两个实体的一个单向连接器。例如,请看下面的命令:
  

ls -1 | wc -l


    该命令首先创建两个进程,一个对应于ls –1,另一个对应于wc –l。然后,把第一个进程的标准输出设为第二个进程的标准输入(如图1所示)。它的作用是计算当前目录下的文件数量。

Linux下的管道编程技术(图一)

点击查看大图

 

图1:管道示意图 



    如上图所示,前面的例子实际上就是在两个命令之间建立了一根管道(有时我们也将之称为命令的流水线操作)。第一个命令ls执行后产生的输出作为了第二个命令wc的输入。这是一个半双工通信,因为通信是单向的。两个命令之间的连接的具体工作,是由内核来完成的。下面我们将会看到,除了命令之外,应用程序也可以使用管道进行连接。

二、信号和消息的区别

    我们知道,进程间的信号通信机制在传递信息时是以信号为载体的,但管道通信机制的信息载体是消息。那么信号和消息之间的区别在哪里呢? 

    首先,在数据内容方面,信号只是一些预定义的代码,用于表示系统发生的某一状况;消息则为一组连续语句或符号,不过量也不会太大。在作用方面,信号担任进程间少量信息的传送,一般为内核程序用来通知用户进程一些异常情况的发生;消息则用于进程间交换彼此的数据。 

    在发送时机方面,信号可以在任何时候发送;信息则不可以在任何时刻发送。在发送者方面,信号不能确定发送者是谁;信息则知道发送者是谁。在发送对象方面,信号是发给某个进程;消息则是发给消息队列。在处理方式上,信号可以不予理会;消息则是必须处理的。在数据传输效率方面,信号不适合进大量的信息传输,因为它的效率不高;消息虽然不适合大量的数据传送,但它的效率比信号强,因此适于中等数量的数据传送。 

    三、管道和命名管道的区别 

    我们知道,命名管道和管道都可以在进程间传送消息,但它们也是有区别的。 

    管道技术只能用于连接具有共同祖先的进程,例如父子进程间的通信,它无法实现不同用户的进程间的信息共享。再者,管道不能常设,当访问管道的进程终止时,管道也就撤销。这些限制给它的使用带来不少限制,但是命名管道却克服了这些限制。 

    命名管道也称为FIFO,是一种永久性的机构。FIFO文件也具有文件名、文件长度、访问许可权等属性,它也能像其它Linux文件那样被打开、关闭和删除,所以任何进程都能找到它。换句话说,即使是不同祖先的进程,也可以利用命名管道进行通信。 

    如果想要全双工通信,那最好使用Sockets API。下面我们分别介绍这两种管道,然后详细说明用来进行管道编程的编程接口和系统级命令。

四、管道编程技术 

    在程序中利用管道进行通信时,根据通信主体大体可以分为两种情况:一种是具有共同祖先的进程间的通信,比较简单;另一种是任意进程间通信,相对较为复杂。下面我们先从较为简单的进程内通信开始介绍。 

    1. 具有共同祖先的进程间通信管道编程 

    为了了解管道编程技术,我们先举一个例子。在这个例中,我们将在进程中新建一个管道,然后向它写入一个消息,管道读取消息后将其发出。代码如下所示: 

    示例代码1:管道程序示例 

1: #include <unistd.h> 2: #include <stdio.h> 3: #include <string.h> 4: 5: #define MAX_LINE 80 6: #define PIPE_STDIN 0 7: #define PIPE_STDOUT 1 8: 9: int main() 10: ...{ 11: const char *string=...{"A sample message."}; 12: int ret, myPipe[2]; 13: char buffer[MAX_LINE+1]; 14: 15: /**//* 建立管道 */ 16: ret = pipe( myPipe ); 17: 18: if (ret == 0) ...{ 19: 20: /**//* 将消息写入管道 */ 21: write( myPipe[PIPE_STDOUT], string, strlen(string) ); 22: 23: /**//* 从管道读取消息 */ 24: ret = read( myPipe[PIPE_STDIN], buffer, MAX_LINE ); 25: 26: /**//* 利用Null结束字符串 */ 27: buffer[ ret ] = 0; 28: 29: printf("%s\n", buffer); 30: 31: } 32: 33: return 0; 34: }

   
    上面的示例代码中,我们利用pipe调用新建了一个管道,参见第16行代码。 我们还建立了一个由两个元素组成的数组,用来描述我们的管道。我们的管道被定义为两个单独的文件描述符,一个用来输入,一个用来输出。我们能从管道的一端输入,然后从另一端读出。如果调用成功,pipe函数返回值为0。返回后,数组myPipe中存放的是两个新的文件描述符,其中元素myPipe[1]包含的文件描述符用于管道的输入,元素myPipe[0] 包含的文件描述符用于管道的输出。

    在第21行代码,我们利用write函数把消息写入管道。站在应用程序的角度,它是在向stdout输出。现在,该管道存有我们的消息,我们可以利用第24行的read函数来读它。对于应用程序来说,我们是利用stdin描述符从管道读取消息的。read函数把从管道读取的数据存放到buffer变量中。然后在buffer变量的末尾添加一个NULL,这样就能利用printf函数正确的输出它了。在本例中的管道可以利用下图解释:

Linux下的管道编程技术(图二)

点击查看大图

 

图2:示例代码1中半双工管道的示意图

    这个例子中,通信是在具有共同祖先的进程间发生的,即父进程和子进程通信。这样做局限性太大,但我们只是用它来给读者一个感性的认识。接下来,我们将介绍更为高级的进程间的管道通信。

 2.进程间通信管道编程 

    在利用管道技术进行编程时,处理要用到上面介绍的pipe函数外,还用到另外三个函数,如下所示。

 pipe函数:该函数用于创建一个新的匿名管道。

 dup函数:该函数用于拷贝文件描述符。

 mkfifo函数:该函数用于创建一个命名管道(fifo)。 

    当然,在管道通信过程中还用到其它函数,到时我们会加以介绍。需要注意的是,说到底,管道无非就是一对文件描述符,因此任何能够操作文件操作符的函数都可以使用管道。这包括但不限于这些函数:select、read、write、 fcntl、freopen,等等。 

    2.1函数pipe 

    函数pipe用来建立一个新的管道,该管道用两个文件描述符进行描述。函数pipe的原型如下所示:

#include <unistd.h> int pipe( int fds[2] );

    当调用成功时,函数pipe返回值为0,否则返回值为-1。成功返回时,数组fds被填入两个有效的文件描述符。数组的第一个元素中的文件描述符供应用程序读取之用,数组的第二个元素中的文件描述符可以用来供应用程序写入。 

    下面我们考察在一个包含多个进程的应用程序中的管道示例。在该程序中(见示例代码2),第14行用于创建一个管道,然后进程在第16行分叉,变成一个父进程和一个子进程。在子进程中,我们尝试从(在第18行建立的)管道的输入描述符读取,这时该进程将被挂起,直到管道中有可以读取的内容为止。 

    读完后,我们用NULL作为读取的内容的结束符,这样的话,读的这些内容就能使用printf函数正确打印输出了。父进程先是利用存放在thePipe[1]中的“写文件标识符”向管道写入测试字符串,然后就使用wait函数来等待子进程退出。 

    在我们的这个程序中需要加以注意的是,我们的子进程是如何继承父进程利用pipe函数建立的文件描述符的,以及如何利用该文件描述符进行通信的。函数fork一旦执行,子进程会继承父进程的功能和管道的文件描述符,但对于内核来说,父进程和子进程是平等的,它们是独立运行的。也就是说,两个进程分别具有单独的内存空间,它们正是通过pipe函数来互通有无的。

示例代码2:演示两个进程间的管道模型的代码 1: #include <stdio.h> 2: #include <unistd.h> 3: #include <string.h> 4: #include <wait.h> 5: 6: #define MAX_LINE 80 7: 8: int main() 9: ...{ 10: int thePipe[2], ret; 11: char buf[MAX_LINE+1]; 12: const char *testbuf=...{"a test string."}; 13: 14: if ( pipe( thePipe ) == 0 ) ...{ 15: 16: if (fork() == 0) ...{ 17: 18: ret = read( thePipe[0], buf, MAX_LINE ); 19: buf[ret] = 0; 20: printf( "Child read %s\n", buf ); 21: 22: } else ...{ 23: 24: ret = write( thePipe[1], testbuf, strlen(testbuf) ); 25: ret = wait( NULL ); 26: 27: } 28: 29: } 30: 31: return 0; 32: }



    需要注意的是,在这个示例程序中我们没有说明如何关闭管道,因为一旦进程结束,与管道有关的资源将被自动释放。尽管如此,为了养成一种良好的编程习惯,最好利用close调用来关闭管道的描述符,如下所示: 

ret = pipe( myPipe ); ... close( myPipe[0] ); close( myPipe[1] );


    如果管道的写入端关闭,但是还有进程尝试从管道读取的话,将被返回0,用来指出管道已不可用,并且应当关闭它。如果管道的读出端关闭,但是还有进程尝试向管道写入的话,试图写入的进程将收到一个SIGPIPE信号,至于信号的具体处理则要视其信号处理程序而定了。

2.2 dup函数和dup2函数

    dup和dup2也是两个非常有用的调用,它们的作用都是用来复制一个文件的描述符。它们经常用来重定向进程的stdin、stdout和stderr。这两个函数的原型如下所示:

#include <unistd.h> int dup( int oldfd ); int dup2( int oldfd, int targetfd )

    利用函数dup,我们可以复制一个描述符。传给该函数一个既有的描述符,它就会返回一个新的描述符,这个新的描述符是传给它的描述符的拷贝。这意味着,这两个描述符共享同一个数据结构。例如,如果我们对一个文件描述符执行lseek操作,得到的第一个文件的位置和第二个是一样的。下面是用来说明dup函数使用方法的代码片段:

int fd1, fd2; ... fd2 = dup( fd1 );

    需要注意的是,我们可以在调用fork之前建立一个描述符,这与调用dup建立描述符的效果是一样的,子进程也同样会收到一个复制出来的描述符。

    dup2函数跟dup函数相似,但dup2函数允许调用者规定一个有效描述符和目标描述符的id。dup2函数成功返回时,目标描述符(dup2函数的第二个参数)将变成源描述符(dup2函数的第一个参数)的复制品,换句话说,两个文件描述符现在都指向同一个文件,并且是函数第一个参数指向的文件。下面我们用一段代码加以说明:

int oldfd; oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 ); dup2( oldfd, 1 ); close( oldfd );

    本例中,我们打开了一个新文件,称为“app_log”,并收到一个文件描述符,该描述符叫做fd1。我们调用dup2函数,参数为oldfd和1,这会导致用我们新打开的文件描述符替换掉由1代表的文件描述符(即stdout,因为标准输出文件的id为1)。任何写到stdout的东西,现在都将改为写入名为“app_log”的文件中。需要注意的是,dup2函数在复制了oldfd之后,会立即将其关闭,但不会关掉新近打开的文件描述符,因为文件描述符1现在也指向它。 

    下面我们介绍一个更加深入的示例代码。回忆一下本文前面讲的命令行管道,在那里,我们将ls –1命令的标准输出作为标准输入连接到wc –l命令。接下来,我们就用一个C程序来加以说明这个过程的实现。代码如下面的示例代码3所示。 

    在示例代码3中,首先在第9行代码中建立一个管道,然后将应用程序分成两个进程:一个子进程(第13–16行)和一个父进程(第20–23行)。接下来,在子进程中首先关闭stdout描述符(第13行),然后提供了ls –1命令功能,不过它不是写到stdout(第13行),而是写到我们建立的管道的输入端,这是通过dup函数来完成重定向的。在第14行,使用dup2函数把stdout重定向到管道(pfds[1])。之后,马上关掉管道的输入端。然后,使用execlp函数把子进程的映像替换为命令ls –1的进程映像,一旦该命令执行,它的任何输出都将发给管道的输入端。 

    现在来研究一下管道的接收端。从代码中可以看出,管道的接收端是由父进程来担当的。首先关闭stdin描述符(第20行),因为我们不会从机器的键盘等标准设备文件来接收数据的输入,而是从其它程序的输出中接收数据。然后,再一次用到dup2函数(第21行),让stdin变成管道的输出端,这是通过让文件描述符0(即常规的stdin)等于pfds[0]来实现的。关闭管道的stdout端(pfds[1]),因为在这里用不到它。最后,使用execlp函数把父进程的映像替换为命令wc -1的进程映像,命令wc -1把管道的内容作为它的输入(第23行)。

示例代码3:利用C实现命令的流水线操作的代码 1: #include <stdio.h> 2: #include <stdlib.h> 3: #include <unistd.h> 4: 5: int main() 6: ...{ 7: int pfds[2]; 8: 9: if ( pipe(pfds) == 0 ) ...{ 10: 11: if ( fork() == 0 ) ...{ 12: 13: close(1); 14: dup2( pfds[1], 1 ); 15: close( pfds[0] ); 16: execlp( "ls", "ls", "-1", NULL ); 17: 18: } else ...{ 19: 20: close(0); 21: dup2( pfds[0], 0 ); 22: close( pfds[1] ); 23: execlp( "wc", "wc", "-l", NULL ); 24: 25: } 26: 27: } 28: 29: return 0; 30: }


     在该程序中,需要格外关注的是,我们的子进程把它的输出重定向的管道的输入,然后,父进程将它的输入重定向到管道的输出。这在实际的应用程序开发中是非常有用的一种技术。

  2.3 mkfifo函数

    mkfifo函数的作用是在文件系统中创建一个文件,该文件用于提供FIFO功能,即命名管道。前边讲的那些管道都没有名字,因此它们被称为匿名管道,或简称管道。对文件系统来说,匿名管道是不可见的,它的作用仅限于在父进程和子进程两个进程间进行通信。而命名管道是一个可见的文件,因此,它可以用于任何两个进程之间的通信,不管这两个进程是不是父子进程,也不管这两个进程之间有没有关系。Mkfifo函数的原型如下所示:

#include <sys/types.h> #include <sys/stat.h> int mkfifo( const char *pathname, mode_t mode );

     mkfifo函数需要两个参数,第一个参数(pathname)是将要在文件系统中创建的一个专用文件。第二个参数(mode)用来规定FIFO的读写权限。Mkfifo函数如果调用成功的话,返回值为0;如果调用失败返回值为-1。下面我们以一个实例来说明如何使用mkfifo函数建一个fifo,具体代码如下所示:

int ret; ... ret = mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 ); if (ret == 0) ...{ // 成功建立命名管道 } else ...{ // 创建命名管道失败 }

    在这个例子中,利用/tmp目录中的cmd_pipe文件建立了一个命名管道(即fifo)。之后,就可以打开这个文件进行读写操作,并以此进行通信了。命名管道一旦打开,就可以利用典型的输入输出函数从中读取内容。举例来说,下面的代码段向我们展示了如何通过fgets函数来从管道中读取内容:

pfp = fopen( "/tmp/cmd_pipe", "r" ); ... ret = fgets( buffer, MAX_LINE, pfp );

    我们还能向管道中写入内容,下面的代码段向我们展示了利用fprintf函数向管道写入的具体方法:

pfp = fopen( "/tmp/cmd_pipe", "w+ ); ... ret = fprintf( pfp, "Here’s a test string!\n" );


    对命名管道来说,除非写入方主动打开管道的读取端,否则读取方是无法打开命名管道的。Open调用执行后,读取方将被锁住,直到写入方出现为止。尽管命名管道有这样的局限性,但它仍不失为一种有效的进程间通信工具。

    上面介绍的是与管道有关的一些系统调用,下面介绍管道命令相关的系统命令。
五、与管道相关的系统命令

    现在开始,我们来研究与进程间通信密切相关的一些系统命令。首先介绍的是mkfifo命令,它的功能与mkfifo系统调用相似,只不过它是用来在命令行中建立一个命名管道。

    在命令行下建立fifo的专用文件,即命名管道的常用方法有两个,mkfifo命令便是其中之一。mkfifo命令的一般用法如下所示:

mkfifo [options] name

 

    这里的options一般为-m,即模式,用以指出读写权限;name是要创建的管道的名称,必要时可以加上路径。如果我们没有规定权限,该命令会采取默认值0644。这里以一个具体实例来说明如何在/tmp目录下面建立一个称为cmd_pipe的命名管道:

 

$ mkfifo /tmp/cmd_pipe

 

    下面用例子说明如何给命名管道指定读写权限。这里我们先将前面建立的管道删掉,然后重新建立管道,并指定管道的权限为0644,当然您也可以指定其他权限:

 

$ rm cmd_pipe $ mkfifo -m 0644 /tmp/cmd_pipe

 

    上面的权限一经建立,就能够在命令行行下通过此管道进行通信了。比如,可以在一个终端上,利用cat命令来读取管道:

 

$ cat cmd_pipe

 

    当输入该命令后,我们的进程就会被挂起,等待写入程序打开此管道。现在,在另一个终端上利用echo命令向这个命名管道写入:

 

$ echo Hi > cmd_pipe

 

    这个命令结束后,要读取该管道的程序(即cat)将被唤醒,然后结束。为醒目起见,这里列出完整的读取方(也就是读取管道的程序)输入的命令和得到的结果:

 

$ cat cmd_pipe Hi $

 

    由此看来,命名管道不仅在C程序中非常有用,而且在脚本中作用也很大。当然,如果组合使用,效果也是很好的。

    除了mkfifo命令外,mknod命令也可以用来创建命名管道,其用法如下所示:

 

$ mknod cmd_pipe p

 


    该命令执行后,将在当前目录下创建一个命名管道cmd_pipe,p用于指出建立的是命名管道。

    六、小结

    在这篇文章中,我们介绍了管道和命名管道的概念,详细的说明了应用程序和命令行创建管道的方法,以及通过它们进行通信的I/O机制。然后,讨论了如何利用dup和 dup2命令来进行输入输出重定向。我们希望本文能够帮您更好的了解Linux下的管道技术。

 


posted @ 2013-01-07 10:05 jackdong 阅读(2810) | 评论 (0)编辑 收藏

http://blog.csdn.net/lixiaoyuaini/article/details/5551852

ubuntu 装好后,默认的ibus输入法只能输入单个汉字,因此需要自己修改。首先在终端卸载ibus:
sudo apt-get remove ibus
完成后运行安装
sudo apt-get install ibus
sudo apt-get install ibus-pinyin


一般系统会提示:

IBus 已经成功启动!如果你不能正常使用 IBus,请将下面代码加入到 $HOME/.bashrc中,并重新登录桌面。
export GTK_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
export QT_IM_MODULE=ibus

因此可以自己修改.bashrc文件。此文件原内容如下蓝色部分所示,红色部分为根据系统添加的内容。

# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples

# If not running interactively, don't do anything
[ -z "$PS1" ] && return

# don't put duplicate lines in the history. See bash(1) for more options
# don't overwrite GNU Midnight Commander's setting of `ignorespace'.
HISTCONTROL=$HISTCONTROL${HISTCONTROL+,}ignoredups
# ... or force ignoredups and ignorespace
HISTCONTROL=ignoreboth

# append to the history file, don't overwrite it
shopt -s histappend

# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)

# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize

# make less more friendly for non-text input files, see lesspipe(1)
[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"

# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then
debian_chroot=$(cat /etc/debian_chroot)
fi

# set a fancy prompt (non-color, unless we know we "want" color)
case "$TERM" in
xterm-color) color_prompt=yes;;
esac

# uncomment for a colored prompt, if the terminal has the capability; turned
# off by default to not distract the user: the focus in a terminal window
# should be on the output of commands, not on the prompt
#force_color_prompt=yes

if [ -n "$force_color_prompt" ]; then
if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
# We have color support; assume it's compliant with Ecma-48
# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
# a case would tend to support setf rather than setaf.)
color_prompt=yes
else
color_prompt=
fi
fi

if [ "$color_prompt" = yes ]; then
PS1='${debian_chroot:+($debian_chroot)}/[/033[01;32m/]/u@/h/[/033[00m/]:/[/033[01;34m/]/w/[/033[00m/]/$ '
else
PS1='${debian_chroot:+($debian_chroot)}/u@/h:/w/$ '
fi
unset color_prompt force_color_prompt

# If this is an xterm set the title to user@host:dir
case "$TERM" in
xterm*|rxvt*)
PS1="/[/e]0;${debian_chroot:+($debian_chroot)}/u@/h: /w/a/]$PS1"
;;
*)
;;
esac

# enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
alias ls='ls --color=auto'
#alias dir='dir --color=auto'
#alias vdir='vdir --color=auto'

alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
fi

# some more ls aliases
#alias ll='ls -l'
#alias la='ls -A'
#alias l='ls -CF'

# Alias definitions.
# You may want to put all your additions into a separate file like
# ~/.bash_aliases, instead of adding them here directly.
# See /usr/share/doc/bash-doc/examples in the bash-doc package.

if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi

# enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
# sources /etc/bash.bashrc).
if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
. /etc/bash_completion
fi


export GTK_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
export QT_IM_MODULE=ibus

 

然后重启。

posted @ 2013-01-05 08:59 jackdong 阅读(358) | 评论 (0)编辑 收藏

http://hi.baidu.com/raybin_yang/item/52fb4315ed99de05d0d66da2

2011-03-04 20:26

Linux ./configure --prefix命令

http://blog.csdn.net/xiyangfan/archive/2010/02/24/5321790.aspx

http://blog.dormforce.net/rox/2008/09/26/configure-prefixxxx%E7%9A%84%E4%BD%9C%E7%94%A8/

源码的安装一般由3个步骤组成:配置(configure)、编译(make)、安装(make install),具体的安装方法一般作者都会给出文档,这里主要讨论配置(configure)。Configure是一个可执行脚本,它有很多选项,使用命令./configure –help输出详细的选项列表,如下:
-bash-3.00# ./configure --help
Usage: configure [options] [host]
Options: [defaults in brackets after descriptions]
Configuration:
--cache-file=FILE     cache test results in FILE
--help             print this message
--no-create         do not create output files
--quiet, --silent     do not print `checking...' messages
--version           print the version of autoconf that created configure
Directory and file names:
--prefix=PREFIX       install architecture-independent files in PREFIX
                [/usr/local]
--exec-prefix=EPREFIX   install architecture-dependent files in EPREFIX
                [same as prefix]
--bindir=DIR         user executables in DIR [EPREFIX/bin]
……….(省略若干)
很多的选项,个人认为,你可以忽略其他的一切,但请把—prefix加上。这里以安装supersparrow-0.0.0为例,我们打算把它安装到目录 /usr/local/supersparrow,于是在supersparrow-0.0.0目录执行带选项的脚本./configure --prefix=/usr/local/supersparrow,执行成功后再编译、安装(make,make install);安装完成将自动生成目录supersparrow,而且该软件所有的文件都被复制到这个目录。为什么要指定这个安装目录?是为了以后的维护方便,如果没有用这个选项,安装过程结束后,该软件所需的软件被复制到不同的系统目录下,很难弄清楚到底复制了那些文件、都复制到哪里去了—基本上是一塌糊涂。

用了—prefix选项的另一个好处是卸载软件或移植软件。当某个安装的软件不再需要时,只须简单的删除该安装目录,就可以把软件卸载得干干净净;移植软件只需拷贝整个目录到另外一个机器即可(相同的操作系统)。

一个小选项有这么方便的作用,建议在实际工作中多多使用

---不指定prefix,可执行文件默认放在/usr /local/bin,库文件默认放在/usr/local/lib,配置文件默认放在/usr/local/etc。其它的资源文件放在/usr /local/share。要卸载这个程序,要么在原来的make目录下用一次make uninstall(前提是make文件指定过uninstall),要么去上述目录里面把相关的文件一个个手工删掉。指定prefix,直接删掉一个文件夹就够了。
posted @ 2013-01-04 14:47 jackdong 阅读(249) | 评论 (0)编辑 收藏

     摘要: http://blog.csdn.net/wooin/article/details/1858917 vim编程idebuffersearchtags 手把手教你把Vim改装成一个IDE编程环境(图文) By: 吴垠 Date: 2007-09-07 Version: 0.5 Email: lazy.fox.wu#gmail.com Homep...  阅读全文
posted @ 2012-12-28 17:36 jackdong 阅读(1268) | 评论 (0)编辑 收藏

     摘要: 以下代码在win7 home basic , ati hd 5450平台测试通过,处理速度为每秒100万次。   程序很简单,只有一个main.cpp程序。Device端只有一个md5.cl文件。 下面我把代码贴出来,因为不能上传附件,我把完整工程包放到了242337476的群共享里面。。。。   main.cpp #include "CL\cl.h" #...  阅读全文
posted @ 2012-12-27 10:46 jackdong 阅读(398) | 评论 (0)编辑 收藏

http://blog.163.com/hotman_x.vip/blog/static/48950133201211483326248/

和所有 Linux 发行版一样,Ubuntu 默认使用 nouveau 开源驱动。话说这个驱动还不错的,甚至能够支持一些三维游戏。不过,既然有更好的 nvidia 官方驱动,弃之不用也说不过去。虽然官方驱动是闭源的,但既然买了 nvidia 的显卡,当然就有使用这个软件的权利。
简要的说,从 Ubuntu 11.10 开始,直到 Ubuntu 12.04,运用官方驱动是很简单的事,无论从“软件中心”用鼠标点击安装,还是用一条命令 sudo apt-get install nvidia-current 来安装,都是简单惬意的事。

然而,在 Ubuntu 12.10 下,出问题了……安装完毕之后,重启,登录,得到了一个没有任何菜单、工具条的纯桌面。好在可以用 Ctrl-Alt T 来启动一个终端——我知道 Ctrl-Alt F1 之类的快捷键,但是,由于中文 locale 设置,在 tty 终端那里,你会得到大量的菱形,它们原本应该是汉字——既然有指挥电脑的地方,那么还有救,呵呵。当年在 Fedora 下一直是手工安装 n 卡驱动的,那个安装过程……很明显就是一个编译的过程!谷歌了一下,果然,需要 kernel 的源代码和头文件!

如果你已经走到了这一步,如下操作(以 curent 版为例,如果你安装的其它版本,比如 current-updates 之类,请自行更改命令)。# 开头的行是注释,请注意 Ubuntu 不允许 root 登录,不会有 root 提示符的(刚写完就想起来,回来修正一下,想得到 root 提示符也很简单的:sudo bash)。

# 首先卸载驱动
$ sudo apt-get remove --purge nvidia-current

# 安装 kernel 源代码、头文件
$ sudo apt-get install linux-source linux-headers-generic

# 重新安装驱动
$ sudo apt-get install nvidia-current

# 通常你会用到的,建议把这个也装上
$ sudo apt-get install nvidia-settings

#重启
$ sudo reboot
posted @ 2012-12-26 12:45 jackdong 阅读(631) | 评论 (0)编辑 收藏

仅列出标题
共10页: 1 2 3 4 5 6 7 8 9 Last