linux多线程编程的书(linux下C/C++多线程远程传输编程问题请教)

2024-07-04 07:16:15 5

linux多线程编程的书(linux下C/C++多线程远程传输编程问题请教)

本文目录

linux下C/C++多线程远程传输编程问题请教

既然是linux下远程传输文件内容,文件数据量较大,而且要保证传输质量的话,自然是选择TCP来传输。***隐藏网址***这只是一个很简单的例子,多进程并发TCP传输需要注意很多问题,不过,在程序运行后会慢慢凸显出来,慢慢改就是。

linux多线程服务端编程 看什么书

这本书主要分享了作者在实现公司内部的分布式服务系统中积累的多线程和网络编程方面的经验,并介绍了C++ 在编写这种分布式系统的服务端程序时的功能取舍与注意事项,书中的很多决策(design decision)是在这一应用场景下做出的。这本书没有细谈分布式系统的设计,只在第9章列举了分布式系统的挑战及其对程序设计(服务端编程)的影响,例如可靠性、可维护性等。

升级Linux操作系统内核奋斗记

升级linux内核奋斗记

我的piii dell机运行着内核2.4.的redhat,自从linux内核2.6发布以来一直想把该系统升级到内核2.6。经过一番艰辛万苦,终于成功了。为了体验一下在linux下编写多线程程序的步骤,又更新gcc和c语言库。在此愿和各位朋友共享这段经历,共享这段成果。(可能有记录错误,仅供参考)。

具体步骤包括以下内容:

1,升级内核的具体步骤

2,更新gcc

3,使用glibc2.3.5

4,使用nptl线程进行编程

在此之前,先简要地介绍一下linux内核2.6所做的改进及新增功能。linux2.6主要在以下的10个方面作了很大的改进,简单概括如下,有关详细情况,请参阅相关资料。

1)通过改善并行处理能力和采用posix线程等,提高了系统的处理能力,从而linux真正具备大型信息系统所要求性能。

2)通过改善文件系统的输出/输入能力和提高对大容量内存的利用效率,使得linux能够更胜任大型信息系统中的数据处理。

3)加强了对数据库应用程序的支持。

4)提高了抗系统高负荷时能力。

5)提高了系统的可用性,包括对不停机时备份和不停机时更换硬件功能的支持。

6)强化了对网络的支持,增加了对ipv6, sctp, ipsec,等的支持。

7)通过增加访问控制和内核暗号化技术,提高了系统的安全性。

8)强化了对电源的管理。

9)强化了对嵌入式cpu和系统的支持。

10)添加了更多的硬件驱动程序。

一.升级内核

1, 下载linux-2.6.3.tar.bz2(可以是更新的版本)到/usr/src

bzcat linux-2.6.3.tar.bz2 | tar xvf – cd linux-2.6.3 阅读readme及changes 文件,确认必须的工具软件。因为不是笔记本电脑和笔记本电脑有关的软件可以忽略。发现module-init-tools需要更新。

2,下载module-init-tools-0.9.13.tar.bz2

bzcat module-init-tools-0.9.13.tar.bz2 | tar xvf – cd module-init-tools-0.9.13 ./configure --prefix=/usr/local/module-init make make moveold make install /usr/local/module-init/sbin/generate-modpobe.conf 》 /etc/modprobe.conf 成功。

3, 编译linux内核

cd /usr/src/linux-2.6.3 make menuconfig 一看太麻烦了,而且许多选项不知所云。退出该程序。 执行make oldconfig,对kernel2.6新增的不熟悉的选项作不选或为缺省。

make bzimage make modules modules_install pwd /usr/src/linux-2.6.3 mv arch/i386/boot/bzimage /boot/bzimage2.6.3 mv system.map /boot/system.map-2.6.3 cd /boot mv system.map oldsystem.map ln –s system.map-2.6.3 system.map 4,建立sys目录

mkdir /sys

5, 修改相关文件,增加对sys的支持

在/etc/rc.d/rc.sysinit文件增加对sys的支持。

1) 找到mount –f /proc的行,在其下面,增加 mount –f /sys

2) 找到 action $ “mounting proc filesystem:” mount –n –t proc /proc /proc 在其下面,增加action $ “mounting sysfs filesystem:” mount –n –t sysfs /sys /sys.

3) 把文件中的 ksyms 改成 kallsyms

在/etc/fstab文件中,增加一行:

none /sys sysfs defaults 0 0

在/etc/rc.d/init.d/halt中,增加对sys的支持,把 awk ‘$2 ~/^/$ | ^/proc | ^/dev / {next} 变为:

awk ‘$2 ~/^/$ | ^/proc | ^/sys | ^/dev / {next}

6,启动

修改/etc/lilo.conf, 增加下面的设置

… image=/boot/bzimange2.6.3 label=linux2.6.3 read-only root=/dev/hda3(由自己的pc设置决定) 成功。

二,更新gcc (因为gcc2.95.3无法编译glibc-2.3.5)

下载gcc-3.4.4.tar.bz2

bzcat gcc-3.4.4.tar.bz2 | tar xvf – cd gcc-3.4.4 ./configure –prefix=/usr/local/gcc344 –enable-shared –enable-threads –enable-threads=posix –enable-languages=c,c++,f77 make bootstrap (因为使用cflags选项时,出错了,所以省略) make install ln –s /usr/local/gcc344/bin/gcc /usr/bin/gcc 三,使用glibc-2.3.5

1)、通过调查发现要编译glibc-2.3.5,要求binutils在2.13以上。所以必须安装binutils-2.14

下载binutils-2.14.tar.gz

tar zxpvf binutils-2.14.tar.gz mkdir binutils-build cd binutils-build ../binutils-2.14/configure –prefix=/usr –enable-shared make tooldir=/usr make check make tooldir=/usr install cp ../binutils-2.14/include/libiberty.h /usr/include 2) 安装glibc-2.3.5

下载 glibc-2.3.5.tar.gz 和 glibc-2.3.5-fix_test-1.patch

tar zxpvf glibc-2.3.5.tar.gz patch –np1 –i ../glibc-2.3.5-fix_test-1.path mkdir glibc-build cd glibc-build ../glibc-2.3.5/configure –prefix=/usr/local/glibc235 –enable-add-ons=linuxthreads –enable-kernel=2.6.0 (若安装在/usr目录下,很危险,可能会损坏你的系统) make make check (必须执行的一步) make localedata /install-locales (对应各国文字) mkdir /usr/man/man3(man的安装路径) make –c ../glibc-2.3.5/linuxthreads/man install 上面的安装完成以后,最好把/usr/local/glibc235/lib和/usr/local/glibc235/include加入到系统的路径中,这样在编译程序时,就不必指定库和header文件的路径了。

四、使用nptl进行线程编程

#include #include #include #include void thread_fun(void * arg); char message = “i am created thread”; int main() { int rnt; pthread_t new_thread; void *thread_result; rnt=pthread_create(&new_thread,null, thread_fun, (void*) message); if (rnt != 0) { perrer (“thread creation failed”); exit(exit_failure); } printf(“waiting for other thread to finish…”); rnt = pthread_join(new_thread, &thread_result); if (rnt != 0) { perrer (“thread join failed”); exit(exit_failure); } printf(“thread join, it returned %s ”, (char*) thread_result); printf(“message now %s”, message); exit(exit_success); } void *thread_fun (void * arg) { printf(“the new thread is running. argument was %s”,(char*)arg); sleep(3); strcpy(message, “bye”); pthread_exit(“thank you for the test”); } 编译

gcc -d_reentrant test_thread.c -o test_thread -lpthread ./test_thread 成功了。

如何看懂《Linux多线程服务端编程

一:进程和线程每个进程有自己独立的地址空间。“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。《Erlang程序设计》把进程比喻为人:每个人有自己的记忆(内存),人与人通过谈话(消息传递)来交流,谈话既可以是面谈(同一台服务器),也可以在电话里谈(不同的服务器,有网络通信)。面谈和电话谈的区别在于,面谈可以立即知道对方是否死了(crash,SIGCHLD),而电话谈只能通过周期性的心跳来判断对方是否还活着。有了这些比喻,设计分布式系统时可以采取“角色扮演”,团队里的几个人各自扮演一个进程,人的角色由进程的代码决定(管登录的、管消息分发的、管买卖的等等)。每个人有自己的记忆,但不知道别人的记忆,要想知道别人的看法,只能通过交谈(暂不考虑共享内存这种IPC)。然后就可以思考:·容错:万一有人突然死了·扩容:新人中途加进来·负载均衡:把甲的活儿挪给乙做·退休:甲要修复bug,先别派新任务,等他做完手上的事情就把他重启等等各种场景,十分便利。线程的特点是共享地址空间,从而可以高效地共享数据。一台机器上的多个进程能高效地共享代码段(操作系统可以映射为同样的物理内存),但不能共享数据。如果多个进程大量共享内存,等于是把多进程程序当成多线程来写,掩耳盗铃。“多线程”的价值,我认为是为了更好地发挥多核处理器(multi-cores)的效能。在单核时代,多线程没有多大价值(个人想法:如果要完成的任务是CPU密集型的,那多线程没有优势,甚至因为线程切换的开销,多线程反而更慢;如果要完成的任务既有CPU计算,又有磁盘或网络IO,则使用多线程的好处是,当某个线程因为IO而阻塞时,OS可以调度其他线程执行,虽然效率确实要比任务的顺序执行效率要高,然而,这种类型的任务,可以通过单线程的”non-blocking IO+IO multiplexing”的模型(事件驱动)来提高效率,采用多线程的方式,带来的可能仅仅是编程上的简单而已)。Alan Cox说过:”A computer is a state machine.Threads are for people who can’t program state machines.”(计算机是一台状态机。线程是给那些不能编写状态机程序的人准备的)如果只有一块CPU、一个执行单元,那么确实如Alan Cox所说,按状态机的思路去写程序是最高效的。二:单线程服务器的常用编程模型据我了解,在高性能的网络程序中,使用得最为广泛的恐怕要数”non-blocking IO + IO multiplexing”这种模型,即Reactor模式。在”non-blocking IO + IO multiplexing”这种模型中,程序的基本结构是一个事件循环(event loop),以事件驱动(event-driven)和事件回调的方式实现业务逻辑: view plain copy //代码仅为示意,没有完整考虑各种情况 while(!done) { int timeout_ms = max(1000, getNextTimedCallback()); int retval = poll(fds, nfds, timeout_ms); if (retval《0){ 处理错误,回调用户的error handler }else{ 处理到期的timers,回调用户的timer handler if(retval》0){ 处理IO事件,回调用户的IO event handler } } } 这里select(2)/poll(2)有伸缩性方面的不足(描述符过多时,效率较低),Linux下可替换为epoll(4),其他操作系统也有对应的高性能替代品。***隐藏网址***基于事件驱动的编程模型也有其本质的缺点,它要求事件回调函数必须是非阻塞的。对于涉及网络IO的请求响应式协议,它容易割裂业务逻辑,使其散布于多个回调函数之中,相对不容易理解和维护。三:多线程服务器的常用编程模型大概有这么几种:a:每个请求创建一个线程,使用阻塞式IO操作。在Java 1.4引人NIO之前,这是Java网络编程的推荐做法。可惜伸缩性不佳(请求太多时,操作系统创建不了这许多线程)。b:使用线程池,同样使用阻塞式IO操作。与第1种相比,这是提高性能的措施。c:使用non-blocking IO + IO multiplexing。即Java NIO的方式。d:Leader/Follower等高级模式。在默认情况下,我会使用第3种,即non-blocking IO + one loop per thread模式来编写多线程C++网络服务程序。1:one loop per thread此种模型下,程序里的每个IO线程有一个event loop,用于处理读写和定时事件(无论周期性的还是单次的)。代码框架跟“单线程服务器的常用编程模型”一节中的一样。libev的作者说:One loop per thread is usually a good model. Doing this is almost never wrong, some times a better-performance model exists, but it is always a good start.这种方式的好处是:a:线程数目基本固定,可以在程序启动的时候设置,不会频繁创建与销毁。b:可以很方便地在线程间调配负载。c:IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发。Event loop代表了线程的主循环,需要让哪个线程干活,就把timer或IO channel(如TCP连接)注册到哪个线程的loop里即可:对实时性有要求的connection可以单独用一个线程;数据量大的connection可以独占一个线程,并把数据处理任务分摊到另几个计算线程中(用线程池);其他次要的辅助性connections可以共享一个线程。比如,在dbproxy中,一个线程用于专门处理客户端发来的管理命令;一个线程用于处理客户端发来的MySQL命令,而与后端数据库通信执行该命令时,是将该任务分配给所有事件线程处理的。对于non-trivial(有一定规模)的服务端程序,一般会采用non-blocking IO + IO multiplexing,每个connection/acceptor都会注册到某个event loop上,程序里有多个event loop,每个线程至多有一个event loop。多线程程序对event loop提出了更高的要求,那就是“线程安全”。要允许一个线程往别的线程的loop里塞东西,这个loop必须得是线程安全的。在dbproxy中,线程向其他线程分发任务,是通过管道和队列实现的。比如主线程accept到连接后,将表示该连接的结构放入队列,并向管道中写入一个字节。计算线程在自己的event loop中注册管道的读事件,一旦有数据可读,就尝试从队列中取任务。2:线程池不过,对于没有IO而光有计算任务的线程,使用event loop有点浪费。可以使用一种补充方案,即用blocking queue实现的任务队列: view plain copy typedef boost::functionFunctor; BlockingQueue taskQueue; //线程安全的全局阻塞队列 //计算线程 void workerThread() { while (running) //running变量是个全局标志 { Functor task = taskQueue.take(); //this blocks task(); //在产品代码中需要考虑异常处理 } } // 创建容量(并发数)为N的线程池 int N = num_of_computing_threads; for (int i = 0; i 《 N; ++i) { create_thread(&workerThread); //启动线程 } //向任务队列中追加任务 Foo foo; //Foo有calc()成员函数 boost::function task = boost::bind(&Foo::calc,&foo); taskQueue.post(task); 除了任务队列,还可以用BlockingQueue实现数据的生产者消费者队列,即T是数据类型而非函数对象,queue的消费者从中拿到数据进行处理。其实本质上是一样的。3:总结总结而言,我推荐的C++多线程服务端编程模式为:one (event) loop per thread + thread pool:event loop用作IO multiplexing,配合non-blockingIO和定时器;thread pool用来做计算,具体可以是任务队列或生产者消费者队列。以这种方式写服务器程序,需要一个优质的基于Reactor模式的网络库来支撑,muduo正是这样的网络库。比如dbproxy使用的是libevent。程序里具体用几个loop、线程池的大小等参数需要根据应用来设定,基本的原则是“阻抗匹配”(解释见下),使得CPU和IO都能高效地运作。所谓阻抗匹配原则:如果池中线程在执行任务时,密集计算所占的时间比重为 P (0 《 P 《= 1),而系统一共有 C 个 CPU,为了让这 C 个 CPU 跑满而又不过载,线程池大小的经验公式 T = C/P。(T 是个 hint,考虑到 P 值的估计不是很准确,T 的最佳值可以上下浮动 50%)以后我再讲这个经验公式是怎么来的,先验证边界条件的正确性。假设 C = 8,P = 1.0,线程池的任务完全是密集计算,那么T = 8。只要 8 个活动线程就能让 8 个 CPU 饱和,再多也没用,因为 CPU 资源已经耗光了。假设 C = 8,P = 0.5,线程池的任务有一半是计算,有一半等在 IO 上,那么T = 16。考虑操作系统能灵活合理地调度 sleeping/writing/running 线程,那么大概 16 个“50%繁忙的线程”能让 8 个 CPU 忙个不停。启动更多的线程并不能提高吞吐量,反而因为增加上下文切换的开销而降低性能。如果 P 《 0.2,这个公式就不适用了,T 可以取一个固定值,比如 5*C。另外,公式里的 C 不一定是 CPU 总数,可以是“分配给这项任务的 CPU 数目”,比如在 8 核机器上分出 4 个核来做一项任务,那么 C=4。四:进程间通信只用TCPLinux下进程间通信的方式有:匿名管道(pipe)、具名管道(FIFO)、POSIX消息队列、共享内存、信号(signals),以及Socket。同步原语有互斥器(mutex)、条件变量(condition variable)、读写锁(reader-writer lock)、文件锁(record locking)、信号量(semaphore)等等。进程间通信我首选Sockets(主要指TCP,我没有用过UDP,也不考虑Unix domain协议)。其好处在于:可以跨主机,具有伸缩性。反正都是多进程了,如果一台机器的处理能力不够,很自然地就能用多台机器来处理。把进程分散到同一局域网的多台机器上,程序改改host:port配置就能继续用;TCP sockets和pipe都是操作文件描述符,用来收发字节流,都可以read/write/fcntl/select/poll等。不同的是,TCP是双向的,Linux的pipe是单向的,进程间双向通信还得开两个文件描述符,不方便;而且进程要有父子关系才能用pipe,这些都限制了pipe的使用;TCP port由一个进程独占,且进程退出时操作系统会自动回收文件描述符。因此即使程序意外退出,也不会给系统留下垃圾,程序重启之后能比较容易地恢复,而不需要重启操作系统(用跨进程的mutex就有这个风险);而且,port是独占的,可以防止程序重复启动,后面那个进程抢不到port,自然就没法初始化了,避免造成意料之外的结果;与其他IPC相比,TCP协议的一个天生的好处是“可记录、可重现”。tcpdump和Wireshark是解决两个进程间协议和状态争端的好帮手,也是性能(吞吐量、延迟)分析的利器。我们可以借此编写分布式程序的自动化回归测试。也可以用tcpcopy之类的工具进行压力测试。TCP还能跨语言,服务端和客户端不必使用同一种语言。分布式系统的软件设计和功能划分一般应该以“进程”为单位。从宏观上看,一个分布式系统是由运行在多台机器上的多个进程组成的,进程之间采用TCP长连接通信。使用TCP长连接的好处有两点:一是容易定位分布式系统中的服务之间的依赖关系。只要在机器上运行netstat -tpna|grep 就能立刻列出用到某服务的客户端地址(Foreign Address列),然后在客户端的机器上用netstat或lsof命令找出是哪个进程发起的连接。TCP短连接和UDP则不具备这一特性。二是通过接收和发送队列的长度也较容易定位网络或程序故障。在正常运行的时候,netstat打印的Recv-Q和Send-Q都应该接近0,或者在0附近摆动。如果Recv-Q保持不变或持续增加,则通常意味着服务进程的处理速度变慢,可能发生了死锁或阻塞。如果Send-Q保持不变或持续增加,有可能是对方服务器太忙、来不及处理,也有可能是网络中间某个路由器或交换机故障造成丢包,甚至对方服务器掉线,这些因素都可能表现为数据发送不出去。通过持续监控Recv-Q和Send-Q就能及早预警性能或可用性故障。以下是服务端线程阻塞造成Recv-Q和客户端Send-Q激增的例子: view plain copy $netstat -tn Proto Recv-Q Send-Q Local Address Foreign tcp 78393 0 10.0.0.10:2000 10.0.0.10:39748 #服务端连接 tcp 0 132608 10.0.0.10:39748 10.0.0.10:2000 #客户端连接 tcp 0 52 10.0.0.10:22 10.0.0.4:55572 五:多线程服务器的适用场合如果要在一台多核机器上提供一种服务或执行一个任务,可用的模式有:a:运行一个单线程的进程;b:运行一个多线程的进程;c:运行多个单线程的进程;d:运行多个多线程的进程;考虑这样的场景:如果使用速率为50MB/s的数据压缩库,进程创建销毁的开销是800微秒,线程创建销毁的开销是50微秒。如何执行压缩任务?如果要偶尔压缩1GB的文本文件,预计运行时间是20s,那么起一个进程去做是合理的,因为进程启动和销毁的开销远远小于实际任务的耗时。如果要经常压缩500kB的文本数据,预计运行时间是10ms,那么每次都起进程 似乎有点浪费了,可以每次单独起一个线程去做。如果要频繁压缩10kB的文本数据,预计运行时间是200微秒,那么每次起线程似 乎也很浪费,不如直接在当前线程搞定。也可以用一个线程池,每次把压缩任务交给线程池,避免阻塞当前线程(特别要避免阻塞IO线程)。由此可见,多线程并不是万灵丹(silver bullet)。1:必须使用单线程的场合据我所知,有两种场合必须使用单线程:a:程序可能会fork(2);实际编程中,应该保证只有单线程程序能进行fork(2)。多线程程序不是不能调用fork(2),而是这么做会遇到很多麻烦:fork一般不能在多线程程序中调用,因为Linux的fork只克隆当前线程的thread of control,不可隆其他线程。fork之后,除了当前线程之外,其他线程都消失了。这就造成一种危险的局面。其他线程可能正好处于临界区之内,持有了某个锁,而它突然死亡,再也没有机会去解锁了。此时如果子进程试图再对同一个mutex加锁,就会立即死锁。因此,fork之后,子进程就相当于处于signal handler之中(因为不知道调用fork时,父进程中的线程此时正在调用什么函数,这和信号发生时的场景一样),你不能调用线程安全的函数(除非它是可重入的),而只能调用异步信号安全的函数。比如,fork之后,子进程不能调用:malloc,因为malloc在访问全局状态时几乎肯定会加锁;任何可能分配或释放内存的函数,比如snprintf;任何Pthreads函数;printf系列函数,因为其他线程可能恰好持有stdout/stderr的锁;除了man 7 signal中明确列出的信号安全函数之外的任何函数。因此,多线程中调用fork,唯一安全的做法是fork之后,立即调用exec执行另一个程序,彻底隔断子进程与父进程的联系。在多线程环境中调用fork,产生子进程后。子进程内部只存在一个线程,也就是父进程中调用fork的线程的副本。使用fork创建子进程时,子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程中的某个线程占有锁,则子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁,并且需要释放哪些锁。尽管Pthread提供了pthread_atfork函数试图绕过这样的问题,但是这回使得代码变得混乱。因此《Programming With Posix Threads》一书的作者说:”Avoid using fork in threaded code except where the child process will immediately exec a new program.”。b:限制程序的CPU占用率;这个很容易理解,比如在一个8核的服务器上,一个单线程程序即便发生busy-wait,占满1个core,其CPU使用率也只有12.5%,在这种最坏的情况下,系统还是有87.5%的计算资源可供其他服务进程使用。因此对于一些辅助性的程序,如果它必须和主要服务进程运行在同一台机器的话,那么做成单线程的能避免过分抢夺系统的计算资源。

linux多线程编程

Linux系统中,多线编程是一种非常常见的编程模型。多线编程可以让程序在多个线程上同时运行,具有提高程序性能和优化CPU利用率的作用。下面是多线编程的基本流程:

1.创建线程:使用pthread_create函数创建需要的线程,这个函数原型如下:

函数参数说明:

thread:用来存放线程ID的指针。

attr:线程属性,通常置NULL。

start_routine:线程运行的函数。

arg:传递给线程运行函数的参数。

2.运行线程:调用pthread_create函数后,程序开始运行线程,并在需要的时候通过pthread_join函数等待线程结束:

函数参数说明:

thread:需要等待的线程的ID。

retval:如果线程没有完全退出,将被存储线程的返回值。

3.终止线程:可以使用pthread_exit函数来终止线程的运行:

函数参数说明:retval:线程的返回值。多线编程需要注意一些问题,例如线程之间的同步问题、共享数据的安全访问等,需要使用互斥锁、条件变量等技术来避免死锁和数据不一致等问题。在编写多线程程序时,需要特别注意这些问题。总之,Linux多线编程是一种非常常见的编程模型,它可以在多个线程上同时运行程序,提高程序性能和优化CPU利用率。但需要注意线程之间的同步问题和数据共享的安全访问等问题,以确保程序可以正确运行。

在linux书上看到,多线程编程需要防止同时访问同一数据,保证数据读写安全

1.运行一个进程中的多个线程,彼此之间使用相同的地址空间,共享大部分数据。2.启动一个线程所花费的空间远远小于启动一个进程所话费的空间。3.线程间切换所需要的时间远远小于进程间切换所需要的时间。4.不同进程具有独立的数据空间,数据的传递只能通过通信的方式。--费时、不方便 统一进程下的线程之间共享数据空间,一个线程数据可以直接为其他线程所用。--快捷、方便5.编写多线程需要注意的地方: 有的变量不能同时被两个线程所修改 子程序中声明为static的数据可能为多线程带来灾难性打击6.多线程优点: 1)提高应用程序相响应,将耗时长的操作置于一个新的线程,避免等待。 2)使CPU多核系统更加高效。 3)改善程序结构。将长而复杂的进程可以分为多个线程。7. 进程是资源分配的基本单位,线程没什么资源。共享进程资源8. volatile的作用是: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值.

一个Linux多进程编程

1 引言对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一:它执行一次却返回两个值。fork函数是Unix系统最杰出的成就之一,它是七十年代UNIX早期的开发者经过长期在理论和实践上的艰苦探索后取得的成果,一方面,它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简洁明了的多进程方法。与DOS和早期的Windows不同,Unix/Linux系统是真正实现多任务操作的系统,可以说,不使用多进程编程,就不能算是真正的Linux环境下编程。多线程程序设计的概念早在六十年代就被提出,但直到八十年代中期,Unix系统中才引入多线程机制,如今,由于自身的许多优点,多线程编程已经得到了广泛的应用。下面,我们将介绍在Linux下编写多进程和多线程程序的一些初步知识。 2 多进程编程什么是一个进程?进程这个概念是针对系统而不是针对用户的,对用户来说,他面对的概念是程序。当用户敲入命令执行一个程序的时候,对系统而言,它将启动一个进程。但和程序不同的是,在这个进程中,系统可能需要再启动一个或多个进程来完成独立的多个任务。多进程编程的主要内容包括进程控制和进程间通信,在了解这些之前,我们先要简单知道进程的结构。 2.1 Linux下进程的结构Linux下一个进程在内存里有三部分的数据,就是"代码段"、"堆栈段"和"数据段"。其实学过汇编语言的人一定知道,一般的CPU都有上述三种段寄存器,以方便操作系统的运行。这三个部分也是构成一个完整的执行序列的必要的部分。"代码段",顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用相同的代码段。"堆栈段"存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc之类的函数取得的空间)。这其中有许多细节问题,这里限于篇幅就不多介绍了。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。 2.2 Linux下的进程控制在传统的Unix环境下,有两个基本的操作用于创建和修改进程:函数fork( )用来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝;函数族exec( )用来启动另外的进程以取代当前运行的进程。Linux的进程控制和传统的Unix进程控制基本一致,只在一些细节的地方有些区别,例如在Linux系统中调用vfork和fork完全相同,而在有些版本的Unix系统中,vfork调用有不同的功能。由于这些差别几乎不影响我们大多数的编程,在这里我们不予考虑。2.2.1 fork( )fork在英文中是"分叉"的意思。为什么取这个名字呢?因为一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就"分叉"了,所以这个名字取得很形象。下面就看看如何具体使用fork,这段程序演示了使用fork的基本框架: void main(){ int i; if ( fork() == 0 ) { /* 子进程程序 */ for ( i = 1; i 《1000; i ++ ) printf("This is child process\n"); } else { /* 父进程程序*/ for ( i = 1; i 《1000; i ++ ) printf("This is process process\n"); } }程序运行后,你就能看到屏幕上交替出现子进程与父进程各打印出的一千条信息了。如果程序还在运行中,你用ps命令就能看到系统中有两个它在运行了。那么调用这个fork函数时发生了什么呢?fork函数启动一个新的进程,前面我们说过,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段;子进程复制父进程的堆栈段和数据段。这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。它们再要交互信息时,只有通过进程间通信来实现,这将是我们下面的内容。既然它们如此相象,系统如何来区分它们呢?这是由函数的返回值来决定的。对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。在操作系统中,我们用ps函数就可以看到不同的进程号,对父进程而言,它的进程号是由比它更低层的系统调用赋予的,而对于子进程而言,它的进程号即是fork函数对父进程的返回值。在程序设计中,父进程和子进程都要调用函数fork()下面的代码,而我们就是利用fork()函数对父子进程的不同返回值用if...else...语句来实现让父子进程完成不同的功能,正如我们上面举的例子一样。我们看到,上面例子执行时两条信息是交互无规则的打印出来的,这是父子进程独立执行的结果,虽然我们的代码似乎和串行的代码没有什么区别。读者也许会问,如果一个大程序在运行中,它的数据段和堆栈都很大,一次fork就要复制一次,那么fork的系统开销不是很大吗?其实UNIX自有其解决的办法,大家知道,一般CPU都是以"页"为单位来分配内存空间的,每一个页都是实际物理内存的一个映像,象INTEL的CPU,其一页在通常情况下是4086字节大小,而无论是数据段还是堆栈段都是由许多"页"构成的,fork函数复制这两个段,只是"逻辑"上的,并非"物理"上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的"页"从物理上也分开。系统在空间上的开销就可以达到最小。下面演示一个足以"搞死"Linux的小程序,其源代码非常简单:void main(){for( ; ; ) fork();}这个程序什么也不做,就是死循环地fork,其结果是程序不断产生进程,而这些进程又不断产生新的进程,很快,系统的进程就满了,系统就被这么多不断产生的进程"撑死了"。当然只要系统管理员预先给每个用户设置可运行的最大进程数,这个恶意的程序就完成不了企图了。2.2.2 exec( )函数族下面我们来看看一个进程如何来启动另一个程序的执行。在Linux中要使用exec函数族。系统调用execve()对当前进程进行替换,替换者为一个指定的程序,其参数包括文件名(filename)、参数列表(argv)以及环境变量(envp)。exec函数族当然不止一个,但它们大致相同,在Linux中,它们分别是:execl,execlp,execle,execv,execve和execvp,下面我只以execlp为例,其它函数究竟与execlp有何区别,请通过manexec命令来了解它们的具体情况。一个进程一旦调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过exec类函数中有的还允许继承环境变量之类的信息。)那么如果我的程序想启动另一程序的执行但自己仍想继续运行的话,怎么办呢?那就是结合fork与exec的使用。下面一段代码显示如何启动运行其它程序: char command; void main() { int rtn; /*子进程的返回数值*/ while(1) { /* 从终端读取要执行的命令 */ printf( "》" ); fgets( command, 256, stdin ); command = 0; if ( fork() == 0 ) { /* 子进程执行此命令 */ execlp( command, command ); /* 如果exec函数返回,表明没有正常执行命令,打印错误信息*/ perror( command ); exit( errorno ); } else { /* 父进程, 等待子进程结束,并打印子进程的返回值 */ wait ( &rtn ); printf( " child process return %d\n",. rtn ); } } } 此程序从终端读入命令并执行之,执行完成后,父进程继续等待从终端读入命令。熟悉DOS和WINDOWS系统调用的朋友一定知道DOS/WINDOWS也有exec类函数,其使用方法是类似的,但DOS/WINDOWS还有spawn类函数,因为DOS是单任务的系统,它只能将"父进程"驻留在机器内再执行"子进程",这就是spawn类的函数。WIN32已经是多任务的系统了,但还保留了spawn类函数,WIN32中实现spawn函数的方法同前述UNIX中的方法差不多,开设子进程后父进程等待子进程结束后才继续运行。UNIX在其一开始就是多任务的系统,所以从核心角度上讲不需要spawn类函数。在这一节里,我们还要讲讲system()和popen()函数。system()函数先调用fork(),然后再调用exec()来执行用户的登录shell,通过它来查找可执行文件的命令并分析参数,最后它么使用wait()函数族之一来等待子进程的结束。函数popen()和函数system()相似,不同的是它调用pipe()函数创建一个管道,通过它来完成程序的标准输入和标准输出。这两个函数是为那些不太勤快的程序员设计的,在效率和安全方面都有相当的缺陷,在可能的情况下,应该尽量避免。 2.3 Linux下的进程间通信详细的讲述进程间通信在这里绝对是不可能的事情,而且笔者很难有信心说自己对这一部分内容的认识达到了什么样的地步,所以在这一节的开头首先向大家推荐著名作者Richard Stevens的著名作品:《Advanced Programming in the UNIX Environment》,它的中文译本《UNIX环境高级编程》已有机械工业出版社出版,原文精彩,译文同样地道,如果你的确对在Linux下编程有浓厚的兴趣,那么赶紧将这本书摆到你的书桌上或计算机旁边来。说这么多实在是难抑心中的景仰之情,言归正传,在这一节里,我们将介绍进程间通信最最初步和最最简单的一些知识和概念。首先,进程间通信至少可以通过传送打开文件来实现,不同的进程通过一个或多个文件来传递信息,事实上,在很多应用系统里,都使用了这种方法。但一般说来,进程间通信(IPC:InterProcess Communication)不包括这种似乎比较低级的通信方法。Unix系统中实现进程间通信的方法很多,而且不幸的是,极少方法能在所有的Unix系统中进行移植(唯一一种是半双工的管道,这也是最原始的一种通信方式)。而Linux作为一种新兴的操作系统,几乎支持所有的Unix下常用的进程间通信方法:管道、消息队列、共享内存、信号量、套接口等等。下面我们将逐一介绍。   2.3.1 管道管道是进程间通信中最古老的方式,它包括无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者用于运行于同一台机器上的任意两个进程间的通信。无名管道由pipe()函数创建:#include 《unistd.h》int pipe(int filedis);参数filedis返回两个文件描述符:filedes的输入。下面的例子示范了如何在父进程和子进程间实现通信。 #define INPUT 0 #define OUTPUT 1 void main() { int file_descriptors; /*定义子进程号 */ pid_t pid; char buf; int returned_count; /*创建无名管道*/ pipe(file_descriptors); /*创建子进程*/ if((pid = fork()) == -1) { printf("Error in fork\n"); exit(1); } /*执行子进程*/ if(pid == 0) { printf("in the spawned (child) process...\n"); /*子进程向父进程写数据,关闭管道的读端*/ close(file_descriptors); write(file_descriptors, "test data", strlen("test data")); exit(0); } else { /*执行父进程*/ printf("in the spawning (parent) process...\n"); /*父进程从管道读取子进程写的数据,关闭管道的写端*/ close(file_descriptors); returned_count = read(file_descriptors, buf, sizeof(buf)); printf("%d bytes of data received from spawned process: %s\n", returned_count, buf); } }在Linux系统下,有名管道可由两种方式创建:命令行方式mknod系统调用和函数mkfifo。下面的两种途径都在当前目录下生成了一个名为myfifo的有名管道:方式一:mkfifo("myfifo","rw");方式二:mknod myfifo p生成了有名管道后,就可以使用一般的文件I/O函数如open、close、read、write等来对它进行操作。下面即是一个简单的例子,假设我们已经创建了一个名为myfifo的有名管道。/* 进程一:读有名管道*/ #include 《stdio.h》 #include 《unistd.h》 void main() { FILE * in_file; int count = 1; char buf; in_file = fopen("mypipe", "r"); if (in_file == NULL) { printf("Error in fdopen.\n"); exit(1); } while ((count = fread(buf, 1, 80, in_file)) 》 0) printf("received from pipe: %s\n", buf); fclose(in_file); }/* 进程二:写有名管道*/ #include 《stdio.h》 #include 《unistd.h》 void main() { FILE * out_file; int count = 1; char buf; out_file = fopen("mypipe", "w"); if (out_file == NULL) { printf("Error opening pipe."); exit(1); } sprintf(buf,"this is test data for the named pipe example\n"); fwrite(buf, 1, 80, out_file); fclose(out_file); } 2.3.2 消息队列消息队列用于运行于同一台机器上的进程间通信,它和管道很相似,事实上,它是一种正逐渐被淘汰的通信方式,我们可以用流管道或者套接口的方式来取代它,所以,我们对此方式也不再解释,也建议读者忽略这种方式。 2.3.3 共享内存共享内存是运行在同一台机器上的进程间通信最快的方式,因为数据不需要在不同的进程间复制。通常由一个进程创建一块共享内存区,其余进程对这块内存区进行读写。得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的将是实际的物理内存,在Linux系统下,这只有通过限制Linux系统存取的内存才可以做到,这当然不太实际。常用的方式是通过shmXXX函数族来实现利用共享内存进行存储的。首先要用的函数是shmget,它获得一个共享存储标识符。#include 《sys/types.h》#include 《sys/ipc.h》#include 《sys/shm.h》int shmget(key_t key, int size, int flag);这个函数有点类似大家熟悉的malloc函数,系统按照请求分配size大小的内存用作共享内存。Linux系统内核中每个IPC结构都有的一个非负整数的标识符,这样对一个消息队列发送消息时只要引用标识符就可以了。这个标识符是内核由IPC结构的关键字得到的,这个关键字,就是上面第一个函数的key。数据类型key_t是在头文件sys/types.h中定义的,它是一个长整形的数据。在我们后面的章节中,还会碰到这个关键字。当共享内存创建后,其余进程可以调用shmat()将其连接到自身的地址空间中。void *shmat(int shmid, void *addr, int flag);shmid为shmget函数返回的共享存储标识符,addr和flag参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址,进程可以对此进程进行读写操作。使用共享存储来实现进程间通信的注意点是对数据存取的同步,必须确保当一个进程去读取数据时,它所想要的数据已经写好了。通常,信号量被要来实现对共享存储数据存取的同步,另外,可以通过使用shmctl函数设置共享存储内存的某些标志位如SHM_LOCK、SHM_UNLOCK等来实现。

linux多线程编程的书(linux下C/C++多线程远程传输编程问题请教)

本文编辑:admin

本文相关文章:


linux多线程编程的书(linux多线程服务端编程 看什么书)

linux多线程编程的书(linux多线程服务端编程 看什么书)

本文目录linux多线程服务端编程 看什么书linux多线程编程linux下C/C++多线程远程传输编程问题请教在linux书上看到,多线程编程需要防止同时访问同一数据,保证数据读写安全如何看懂《Linux多线程服务端编程一个Linux多进

2024年7月24日 06:38

更多文章:


如何让Windows 10系统桌面变得更好看?win10桌面的简单美化

如何让Windows 10系统桌面变得更好看?win10桌面的简单美化

大家好,如果您还对桌面美化不太了解,没有关系,今天就由本站为大家分享桌面美化的知识,包括如何让Windows 10系统桌面变得更好看的问题都会给大家分析到,还望可以解决大家的问题,下面我们就开始吧!本文目录如何让Windows 10系统桌面

2024年7月26日 13:01

微信会员卡管理系统(微信会员卡是怎么实现的)

微信会员卡管理系统(微信会员卡是怎么实现的)

大家好,今天小编来为大家解答以下的问题,关于微信会员卡管理系统,微信会员卡是怎么实现的这个很多人还不知道,现在让我们一起来看看吧!本文目录微信会员卡是怎么实现的微信会员卡管理系统多少钱微信会员系统具体是怎么做的美发店微信会员卡管理系统如何做

2024年10月14日 19:35

msvcr100 dll放哪里(msvcp100.dll丢失怎么办放在哪里)

msvcr100 dll放哪里(msvcp100.dll丢失怎么办放在哪里)

本文目录msvcp100.dll丢失怎么办放在哪里msvcr100.dll放在哪里围攻》的msvcr100.dll文件应该放在哪个文件夹msvcr100.dll放哪里msvcp100.dll丢失怎么办放在哪里下载到该DLL文件后将msvcp

2024年6月3日 02:32

星际争霸2手游单机版下载中文版(《星际争霸2》怎么玩单机的)

星际争霸2手游单机版下载中文版(《星际争霸2》怎么玩单机的)

大家好,关于星际争霸2手游单机版下载中文版很多朋友都还不太明白,不过没关系,因为今天小编就来为大家分享关于《星际争霸2》怎么玩单机的的知识点,相信应该可以解决大家的一些困惑和问题,如果碰巧可以解决您的问题,还望关注下本站哦,希望对各位有所帮

2024年6月13日 03:24

图片编辑工具(编辑照片的软件哪个好)

图片编辑工具(编辑照片的软件哪个好)

各位老铁们,大家好,今天由我来为大家分享图片编辑工具,以及编辑照片的软件哪个好的相关问题知识,希望对大家有所帮助。如果可以帮助到大家,还望关注收藏下本站,您的支持是我们最大的动力,谢谢大家了哈,下面我们开始吧!本文目录编辑照片的软件哪个好有

2024年7月23日 07:21

手机键盘输入法下载(怎样设置手机键盘输入方式)

手机键盘输入法下载(怎样设置手机键盘输入方式)

大家好,手机键盘输入法下载相信很多的网友都不是很明白,包括怎样设置手机键盘输入方式也是一样,不过没有关系,接下来就来为大家分享关于手机键盘输入法下载和怎样设置手机键盘输入方式的一些知识点,大家可以关注收藏,免得下次来找不到哦,下面我们开始吧

2024年6月22日 02:49

天谕生活技能(天谕手游生活技能新手有几级)

天谕生活技能(天谕手游生活技能新手有几级)

本文目录天谕手游生活技能新手有几级最近在玩天谕,想做个休闲玩家,生活技能需要怎么练,我打算主攻裁缝,生活技能是不是也需要地区声望,裁天谕鲤鱼王用什么鱼饵天谕生活技能赚钱方法 天谕怎么用生活技天谕生活技能怎么练天谕副职业准考证在哪买天谕生活技

2024年1月1日 06:00

腾讯qq代码(腾讯qq错误代码116705是什么意思)

腾讯qq代码(腾讯qq错误代码116705是什么意思)

本文目录腾讯qq错误代码116705是什么意思QQ软件的代码有多少腾讯官方认证qq代码是多少腾讯QQ公司的股票代码是多少呢2012年6月腾讯QQ最新百分百能用的Q币代码qq代码怎么用qq版本各个版本的代码,我要包括现在最新的,谢谢~!腾讯q

2024年5月26日 20:07

我的世界不一样(我想问一下,为什么我的手机网易版我的世界和别人主页都不一样)

我的世界不一样(我想问一下,为什么我的手机网易版我的世界和别人主页都不一样)

本文目录我想问一下,为什么我的手机网易版我的世界和别人主页都不一样我要怎么下载不一样的我的世界为什么手机上的我的世界和有些主播玩的不一样,别的主播画质很好,我的就不好为什么我的世界和别人的不一样我的世界手机版和电脑版有什么不同网易我的世界电

2024年7月13日 16:54

弈城围棋水平对照表(围棋等级的划分,简单说一下就行)

弈城围棋水平对照表(围棋等级的划分,简单说一下就行)

各位老铁们,大家好,今天由我来为大家分享弈城围棋水平对照表,以及围棋等级的划分,简单说一下就行的相关问题知识,希望对大家有所帮助。如果可以帮助到大家,还望关注收藏下本站,您的支持是我们最大的动力,谢谢大家了哈,下面我们开始吧!本文目录围棋等

2024年7月28日 07:50

autocad2007画图教程(cad二维设计画图教程)

autocad2007画图教程(cad二维设计画图教程)

大家好,如果您还对autocad2007画图教程不太了解,没有关系,今天就由本站为大家分享autocad2007画图教程的知识,包括cad二维设计画图教程的问题都会给大家分析到,还望可以解决大家的问题,下面我们就开始吧!本文目录cad二维设

2024年8月4日 12:55

腾讯首页qq(QQ与腾讯首页问题)

腾讯首页qq(QQ与腾讯首页问题)

本文目录QQ与腾讯首页问题打开QQ腾讯首页的问题为什么腾讯QQ首页无法显示QQ与腾讯首页问题如果你是会员的话才可以取消弹出迷你首页,不是的话只要你登陆就会自动弹出,如果关了想看的话可以点击如下图:QQ个人设置-系统设置-基本设置如果你是会员

2024年1月2日 12:00

网络安全教育知识(大学生网络安全知识有哪些)

网络安全教育知识(大学生网络安全知识有哪些)

“网络安全教育知识”相关信息最新大全有哪些,这是大家都非常关心的,接下来就一起看看网络安全教育知识(大学生网络安全知识有哪些)!本文目录大学生网络安全知识有哪些网络安全基础知识大全网络安全教育知识有哪些小学生应该如何进行网络安全教育网络安全

2024年9月27日 04:35

排超联赛积分榜(历届中超积分榜排名)

排超联赛积分榜(历届中超积分榜排名)

本篇文章给大家谈谈排超联赛积分榜,以及历届中超积分榜排名对应的知识点,文章可能有点长,但是希望大家可以阅读完,增长自己的知识,最重要的是希望对各位有所帮助,可以解决了您的问题,不要忘了收藏本站喔。本文目录历届中超积分榜排名中超联赛积分榜排名

2024年10月24日 11:45

s4 root教程(GALAXY S4 如何获取ROOT权限)

s4 root教程(GALAXY S4 如何获取ROOT权限)

本文目录GALAXY S4 如何获取ROOT权限三星GALAXYS4怎么获得ROOT权限怎样获取s4的root权限奇兔一键ROOT教程---三星S4获取ROOT权限的方法GALAXY S4 如何获取ROOT权限使用towelroot工具进行

2023年6月18日 15:00

公安交警官网 违章查询(交警网违章查询)

公安交警官网 违章查询(交警网违章查询)

其实公安交警官网 违章查询的问题并不复杂,但是又很多的朋友都不太了解交警网违章查询,因此呢,今天小编就来为大家分享公安交警官网 违章查询的一些知识,希望可以帮助到大家,下面我们一起来看看这个问题的分析吧!本文目录交警网违章查询小型汽车违章怎

2024年5月16日 19:53

电子音乐相册制作(怎样制作音乐相册上快手)

电子音乐相册制作(怎样制作音乐相册上快手)

其实电子音乐相册制作的问题并不复杂,但是又很多的朋友都不太了解怎样制作音乐相册上快手,因此呢,今天小编就来为大家分享电子音乐相册制作的一些知识,希望可以帮助到大家,下面我们一起来看看这个问题的分析吧!本文目录怎样制作音乐相册上快手微信朋友圈

2024年5月22日 05:14

长虹手机官网(长虹手机的虹是代表什么意思)

长虹手机官网(长虹手机的虹是代表什么意思)

本文目录长虹手机的虹是代表什么意思长虹虹手机多少钱长虹手机官网长虹手机的虹是代表什么意思虹phone的全名是——长虹智能手机虹phone。这款手机于4月10日正式在长虹手机官网宣布可以预订,而且现在预订的话,可享“长虹惠民价”1990元(市

2023年7月17日 07:40

召唤兽初值计算器(怎么算大话召唤兽的初值)

召唤兽初值计算器(怎么算大话召唤兽的初值)

本文目录怎么算大话召唤兽的初值大话2召唤兽计算器大话西游2中的召唤兽怎么查初值大话西游2现在召唤兽的初值计算器怎么不好用了呢感觉为什么没有大话2 当康召唤兽的计算器啊大话2原始敏和实际初敏怎么算大话2主页的召唤兽数据预测是不是错的,为什么现

2024年6月4日 19:35

球球大作战刷棒棒糖(球球大作战怎么刷棒棒糖 拿棒棒糖有什么用)

球球大作战刷棒棒糖(球球大作战怎么刷棒棒糖 拿棒棒糖有什么用)

本文目录球球大作战怎么刷棒棒糖 拿棒棒糖有什么用球球大作战刷棒棒糖方法攻略有哪些什么是刷棒棒糖球球大作战刷棒棒糖攻略 怎么一次性刷100个棒棒糖《球球大作战》刷10个棒棒糖的方法球球大作战怎么刷无限棒棒糖球球大作战怎么快速获得棒棒糖球球大作

2024年5月9日 17:05

近期文章

本站热文

iphone vpn设置(ios设置vpn快捷开关)
2024-07-22 15:01:12 浏览:2342
windows12正式版下载(操作系统Windows Server 2012 R2,在哪能下载到,公司用的)
2024-07-20 17:26:53 浏览:1736
client mfc application未响应(每次进cf就提示client MFC Application未响应该怎么办啊!急急急)
2024-07-20 11:15:58 浏览:1168
java安装教程(win10如何安装JAVA)
2024-07-19 19:55:49 浏览:1164
标签列表

热门搜索