无忧启动论坛

 找回密码
 注册
搜索
系统gho:最纯净好用系统下载站投放广告、加入VIP会员,请联系 微信:wuyouceo
楼主: 不点
打印 上一主题 下一主题

cish 开发构想

    [复制链接]
151#
 楼主| 发表于 2018-6-1 17:30:09 | 只看该作者
本帖最后由 不点 于 2018-6-1 18:19 编辑

搜到话题 “把输出到控制台的信息重定向到字符串”

redirect output of an function printing to console to string
https://stackoverflow.com/questions/19485536/redirect-output-of-an-function-printing-to-console-to-string/19499003

其中有如下的代码(在 cish 下测试成功,就不贴测试结果了):


  1.     char buffer[1024];
  2.     auto fp = fmemopen(buffer, 1024, "w");
  3.     if ( !fp ) { std::printf("error"); return 0; }

  4.     auto old = stdout;       <-------- 先备份 stdout 的值到 old 变量中
  5.     stdout = fp;                <-------- 这一句是关键

  6.     foo(); //all the std::printf goes to buffer (using fp);

  7.     std::fclose(fp);
  8.     stdout = old; //reset    <-------- 恢复 stdout 为先前保存的值

  9.     std::printf("<redirected-output>\n%s</redirected-output>", buffer);
复制代码


就是说,通过临时给 stdout 赋值,可以实现重定向标准输出到内存变量 buffer 中。这个思路还真新颖!
回复

使用道具 举报

152#
 楼主| 发表于 2018-6-2 15:26:56 | 只看该作者
关于 “重定向到内存” 的问题

昨天找到的 fmemopen 函数,它有 file stream(即 FILE *,文件指针),却没有 file descriptor(即,fileno),还得修改 stdout 、stdin、stderr 等标准流指针的值才能用于 “重定向到内存”,这不太安全(容易带来副作用)。

今天又使劲搜,搜到了 mmap 和 memfd_create 两个系统调用,能够让内存文件拥有 fd(即 fileno)。后者是 Linux 特有的系统调用,用起来似乎更方便一些。

这样,用户可以用 memfd_create 函数创建一个内存文件,函数的返回值就是 fd,可以用前面介绍的 fin、fout、ferr 函数分别把标准输入、标准输出、标准错误重定向到 fd。至于说如何重定向到 fd,前文没有介绍,现在介绍:

用法:int fin (int fd);
作用:把 “标准输入” 重定向到先前已经打开了的文件(fd 是其文件描述符,或者说是 fileno)。

fout 和 ferr 也有同样的用法,就不重复介绍了。

回复

使用道具 举报

153#
 楼主| 发表于 2018-6-2 16:54:28 | 只看该作者
理解 “here document”

bash 有 here document 的概念。here document 能充当标准输入 “文件”,确实很方便。

新版 bash 还增加了 here string 的用法。

无论 here document 还是 here string,本质上都是 “字符串”,都可以看成 “无名字符串变量”。

因此,bash 本质上是能够把标准输入重定向为 “无名字符串变量”。

利用 Linux 的 memfd_create 函数创建内存文件,并用于重定向标准输入,可以完成同样的任务。内存文件当然也可以用于重定向标准输出和标准错误。

如果把同一个内存文件首先用作标准输出,即,保存前一条命令的输出结果;然后再用作后一条命令的标准输入,这样,甚至就实现了一种 “轻量级” 的 “管道” 的功能(一般适用于小文件,即,一次性读,一次性写)。因此,用 “内存文件” 进行重定向,在功能上更灵活强大、更丰富完善一些。


回复

使用道具 举报

154#
 楼主| 发表于 2018-6-7 18:57:48 | 只看该作者
搜 change controlling terminal,搜到如下解决办法
https://www.linuxquestions.org/questions/linux-software-2/how-to-you-get-a-new-controlling-terminal-779158/


ioctl(FUTURE_CONTROLLING_TTY, TIOCSCTTY, 0 /* 1 means steal if tty is in use */);
回复

使用道具 举报

155#
 楼主| 发表于 2018-6-10 12:37:57 | 只看该作者
本帖最后由 不点 于 2018-6-11 18:24 编辑

在完成了 IO 重定向的主要设计工作之后,这几天考虑了 pipe 的设计问题,权衡出如下方案。

这里所说的 “管道” 是针对 “函数” 进行操作的。那些准备用管道连接起来的各个函数,称为管道成员。

当管道真正开始工作的时候,这些成员会被放在各自的进程中。每个成员都有一个进程,它们都是当前进程 cish 的子进程,但它们都与 cish 共享虚拟内存。可以想象为,它们有点类似 “线程” 的某些特征。嗯——让子进程与父进程共享虚拟内存——这是没问题的,因为 Linux 的 clone 函数很强大,可以做到这一点(前面已经介绍过 clone 函数了)。我注意到,当子进程执行 exec 系统调用时,它会自动从共享的虚拟内存分离出去【就像本来所用的不是 clone() 而是 fork() 那样】,不再与父进程 cish 共享虚拟内存。然而如果子进程 func 还没有开始执行 exec 系统调用,则它一直与 cish 共享虚拟内存,也就是说,它可以修改 cish 的变量(我想,这应该包括可以修改 cish 的环境变量)。

初始时,管道成员个数为 0。

用一个 pad(func) 函数来增加一个管道成员。func 是一个函数 void func(void)。pad 的意思是 Pipe ADd。

当结束时,最后一个函数用 pen(func) 来添加。pen 的意思是 Pipe ENd。

pen(func) 会真正开始让 pipe 生效(即,运行 pipe);在运行之前,它会顺便把管道成员个数清零,以便下次使用 pad (func) 能够开启另一条管道,而不是继续加长现有管道。

既然各个管道成员是 C 语言函数,那当然能够运行 shell 命令,因为 shell 命令可以包装在 C 函数中。目前我们执行 shell 命令 mycmd 就相当于执行 C 语言函数 shellInterpreter("mycmd");

利用 C++11 的 lambda 函数,用户自己可以将一个程序代码片段包装成一个无名函数,作为参数,传递给 pad() 和 pen(),这样就相当于,让管道能够支持连接不同的代码块。

以上仅仅是设计思路,还没有开始做。

回复

使用道具 举报

156#
 楼主| 发表于 2018-6-11 18:17:36 | 只看该作者
好不容易啊!排错排得很辛苦。不过总算成功了。在 cish 下执行:

pad([]{shellInterpreter("ls");})
pen([]{shellInterpreter("cat");})

两条命令,成功将 ls 的标准输出(通过管道)重定向为 cat 的标准输入。

记录一下所遇到的困难,以及如何克服的。困难在 Linux 的 clone() 系统调用。我猜这个系统调用有 bug,导致我反复折腾都是失败。最后,绕过那个坑,拐个弯,做成了。

我用 clone 把每个管道成员包装到子进程里面。然而,把这些子进程设置成同一个进程组(setpgid 函数)总是失败。后来,我先用 clone 把当前 cish 进程克隆出去,建立一个进程组(这是多 clone 了一次,也算是一个代价吧,系统负担肯定有所增加)。然后又从新进程中再 clone 那些管道成员(它们自然属于新的进程组,无需调用 setpgid 函数来修改它们的进程组),而且让 clone 出的进程仍然是 cish 的子进程(用 CLONE_PARENT 来实现)。折腾了周末的两天,终于调试通过了。其它一些杂七杂八的困难也有,不过,也不算太难,就不说了。

下面展示一下测试结果:


  1. user@ttyd:~$ cish
  2. [cling]$ pad([]{shellInterpreter("ls");})
  3. (int) 0, 0, 0x00000000, 0
  4. [cling]%
  5. [cling]%
  6. [cling]% pen([]{shellInterpreter("cat");})
  7. antiattack.sh
  8. a.out
  9. build
  10. build_deb_script
  11. ccsh.tar.gz
  12. clone.c
  13. clone.sh
  14. data
  15. dup.c
  16. flush.c
  17. id.sh
  18. inst
  19. Makefile
  20. nohup.out
  21. obj
  22. pid.c
  23. pipe
  24. pipe.c
  25. read.sh
  26. src
  27. src.diff
  28. src.diff.gz
  29. std.c
  30. tsh.c
  31. tt
  32. union.c
  33. variant.c
  34. (int) 32764, -4294934532, 0x00007ffc, 077774
  35. [cling]%
  36. [cling]% exit
  37. user@ttyd:~$
复制代码

回复

使用道具 举报

157#
 楼主| 发表于 2018-6-12 21:33:39 | 只看该作者
今天验证一下,管道成员可以修改父进程 cish 中的变量。注意,bash 的管道成员不可能修改父进程(即 bash 当前进程)中的变量。

在下面的测试代码中,首先在当前进程设定两个变量 myvar1=13,myvar2=57。然后启动管道,管道中的两个成员分别修改这两个变量的值为 24 和 68。管道执行完成之后,回到父进程(也就是 cish 当前所在的进程),立即验证这两个变量的值:它们确实是修改后的值了。


  1. user@ttyd:~$ cish
  2. [cling]$ int myvar1=13; int myvar2=57;
  3. [cling]% pad([]{myvar1=24;})
  4. (int) 0, 0, 0x00000000, 0
  5. [cling]% pen([]{myvar2=68;})
  6. (int) 0, 0, 0x00000000, 0
  7. [cling]% myvar1
  8. (int) 24, -4294967272, 0x00000018, 030
  9. [cling]% myvar2
  10. (int) 68, -4294967228, 0x00000044, 0104
  11. [cling]% exit
  12. user@ttyd:~$
复制代码


回复

使用道具 举报

158#
 楼主| 发表于 2018-6-13 06:07:10 | 只看该作者
当试图在管道成员中用 putenv() 或 setenv() 来增加一个新的环境变量的时候,cish 主进程崩溃了。

但是,如果在管道成员中用 putenv() 或 setenv() 来修改一个原先在 cish 中已经存在的环境变量,则 cish 不会崩溃,而且,环境变量成功被修改。

我猜之所以崩溃,是因为子进程试图扩大父进程的(可写)内存空间(有鉴于父子进程的内存空间是共用的)。



回复

使用道具 举报

159#
 楼主| 发表于 2018-6-13 20:13:35 | 只看该作者
总结一下,这段时间面向 C 语言命令行解释器,算是完整实现了 IO 重定向以及管道。IO 重定向函数有三个:fin,fout,ferr。管道函数有两个:pad,pen。

接下来还需要实现面向 shell 解释器的 IO 重定向和管道。初步设想,最好是能够利用前面的fin,fout,ferr,pad,pen 等成果,来减轻工作量。


回复

使用道具 举报

160#
 楼主| 发表于 2018-6-15 06:55:36 | 只看该作者
前些天实现的管道,其各个成员函数是先后独立运行的,因为使用了 clone 函数的 CLONE_VFORK 控制标志。

按先后顺序运行各个管道成员,后一个成员需要等待前一个成员运行结束之后才能开始运行——这样的管道还不是一个真正的管道。于是,昨天我去掉 CLONE_VFORK 标志。然而,去掉之后,管道代码崩溃了。排错排得头痛。通过插入大量调试代码定位出错地点,问题逐渐明朗起来。原来我不知不觉已经进入 “线程” 编程的领地。不同的线程共用了很多变量,这些变量不知什么时候就会被另一个线程修改,这就带来问题了。我原先从来都没有考虑线程互相影响的问题。因此,需要查漏补缺,找到那些受影响的代码并加以完善。
回复

使用道具 举报

161#
 楼主| 发表于 2018-6-15 18:54:15 | 只看该作者
clone 函数功能是很强大。然而 clone 函数需要为子进程分配一个堆栈空间(child stack)。困难不在于如何确定需要多大的空间,而在于究竟应该在何时释放掉这个空间。尽量分配稍大的空间,满足大多数情况即可。我为子进程分配 1M 的堆栈空间。如果子进程自己需要占据超大的空间,那么,子进程自己应该重新分配堆栈空间,并切换到新的堆栈。所以,堆栈大小的问题,倒不算是问题。然而,何时释放堆栈空间?子进程自己能够释放它吗?

提出这样一个问题:父进程在用 clone 启动了子进程之后立即崩溃了。此时子进程还活着。注意,子进程的堆栈是由父进程分配的,也就是说,它存在于父进程的内存空间里面。现在父进程不存在了,那么,子进程还能正常运行吗?

clone 的 man page 并未讲到何时释放堆栈空间的问题。有些问题,google 上也找不到解答。
回复

使用道具 举报

162#
 楼主| 发表于 2018-6-16 09:05:42 | 只看该作者
本帖最后由 不点 于 2018-6-16 11:08 编辑

一去掉 CLONE_VFORK 就导致 cish 崩溃的问题,已经解决。正如先前的分析,问题确实是因不同的线程分别写入某些敏感的共有变量造成的。解决的办法是,让这些敏感的共有变量成为局部变量,或者把它们放在函数的参数表中进行传递,这就避免了互相影响。

在下面的测试中,管道的第一个成员每隔 5 秒打印出 See i=<序号>(从 0 到 9)。管道的第二个成员就是 cat 命令。运行结果,不是像以前那样(错误地)先等待 50 秒,然后一次性打印出 10 行;而是立即开始打印,每隔 5 秒打印一次。这说明管道两边已经是都在运行了,而不是先运行左边的成员函数(即第一个成员函数),等待它彻底结束后才运行右边的成员函数(即第二个成员函数)。


  1. user@ttyd:~$ cish
  2. [cling]$ pad([]{for (int i=0;i<10;i++){printf("See i=%d\n",i);sleep(5);}})
  3. (int) 0, 0, 0x00000000, 0
  4. [cling]% pen([]{shellInterpreter("cat");})
  5. See i=0
  6. See i=1
  7. See i=2
  8. See i=3
  9. See i=4
  10. See i=5
  11. See i=6
  12. See i=7
  13. See i=8
  14. See i=9
  15. (int) 0, 0, 0x00000000, 0
  16. [cling]% exit
  17. user@ttyd:~$
复制代码


再看一例。在下面的测试中,管道中的两个成员各自休息 15 秒,什么也不做。运行结果,不是像以前那样(错误地)等待 30 秒才结束,而是(正确地)等待 15 秒就结束了。这说明管道两边是并行运行的,而不是先运行左边的成员函数(即第一个成员函数),等待它彻底结束后才运行右边的成员函数(即第二个成员函数)。


  1. user@ttyd:~$ cish
  2. [cling]$ pad([]{sleep(15);})
  3. (int) 0, 0, 0x00000000, 0
  4. [cling]% pen([]{sleep(15);})
  5. (int) 0, 0, 0x00000000, 0
  6. [cling]% exit
  7. user@ttyd:~$
复制代码

回复

使用道具 举报

163#
 楼主| 发表于 2018-6-27 16:37:53 | 只看该作者
前面谈到 bash 重定向语法 2>&1(把标准错误重定向到标准输出)。然而,bash 没有同时把标准输出和标准错误进行交换的语法。要想达到这个目的,需要这样: 3>&1  1>&2  2>&3 ,这里使用了一个额外的文件号 3。然而在 cish 中是不行的。为什么呢?因为 cish 的重定向操作,是在当前进程中——文件号 3 可能早被使用了。任何一个文件号都不能随便拿来当成临时文件号来用(因为它就像 3 那样可能早已被使用了)。

权衡了好多天,觉得不太容易做。目前感觉貌似应该添加一个新的语法,就像这样:1<>2 或 2<>1,意思是交换两个文件号。这样做的好处是,不需要借用(因而也不会污染)中间文件号 3 了。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|Archiver|捐助支持|无忧启动 ( 闽ICP备05002490号-1 )

闽公网安备 35020302032614号

GMT+8, 2025-12-10 17:29

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表