震驚!ConcurrentHashMap裏面也有死循環,作者留下的“彩蛋”了解一下?

JDK BUG

這篇文章,聊一下我最近才知道的一個關於 JDK 8 的 BUG 吧。

首先說一下我是怎麼發現這個 BUG 的呢?

大家都知道我對 Dubbo 有一定的關注,前段時間 Dubbo 2.7.7 發布后我看了它的更新點,就是下面這個網址: https://github.com/apache/dubbo/releases/tag/dubbo-2.7.7

其中有 Bugfixes 這一部分:

每一個我都去簡單的看了一下,其他的 Bugfixes 或多或少都和 Dubbo 框架有一定的關聯性。但是上面紅框框起來的部分完全就是 JDK 的 Bug 了。

所以可以單獨拎出來說。

這個 Bug 我也是看到了這個地方才知道的,但是研究的過程中我發現,這個怎麼說呢:我懷疑這根本就不是 Bug ,這就是 Doug Lea 老爺子在釣魚執法。

為什麼這樣的說呢,大家看完本文就知道了。

Bug 穩定復現

點擊 Dubbo 裏面的鏈接,我們可以看到具體的描述就是一個鏈接:

打開這個鏈接:

https://bugs.openjdk.java.net/browse/JDK-8062841

我們可以看到:這個 Bug 是位於大名鼎鼎的 concurrent 包裏面的 computeIfAbsent 方法。

這個 Bug 在 JDK 9 裏面被修復了,修復人是 Doug Lea。

而我們知道 ConcurrentHashMap 就是 Doug Lea 的大作,可以說是“誰污染誰治理”。

要了解這個 Bug 是怎麼回事,就必須先了解下面這個方法是幹啥的:

java.util.concurrent.ConcurrentHashMap#computeIfAbsent

從這個方法的第二個入參 mappingFunction 我們可以知道這是 JDK 8 之後提供的方法了。

該方法的含義是:當前 Map 中 key 對應的值不存在時,會調用 mappingFunction 函數,並且將該函數的執行結果(不為 null)作為該 key 的 value 返回。

比如下面這樣的:

初始化一個 ConcurrentHashMap ,然後第一次去獲取 key 為 why 的 value,沒有獲取到,直接返回 null。

接着調用 computeIfAbsent 方法,獲取到 null 后調用 getValue 方法,將該方法的返回值和當前的 key 關聯起來。

所以,第二次獲取的時候拿到了 “why技術”。

其實上面的代碼的 17 行的返回值就是 “why技術”,只是我為了代碼演示,再去調用了一次 map.get() 方法。

知道這個方法干什麼的,接下來就帶大家看看 Bug 是什麼。

我們直接用這個問題裏面給的測試用例,地址:

https://bugs.openjdk.java.net/secure/attachment/23985/Main.java

我只是在第 11 行和第 21 行加入了輸出語句:

正常的情況下,我們希望方法正常結束,然後 map 裏面是這樣的:{AaAa=42,BBBB=42}

但是你把這個代碼拿到本地去跑(需要 JDK 8 環境),你會發現,這個方法永遠不會結束。因為它在進行死循環。

這就是 Bug。

提問的藝術

知道 Bug 了,按理來說就應該開始分析源碼,了解為啥出現了會出現這個 Bug。

但是我想先插播一小節提問的藝術。因為這個 Bug 就是一個活生生的示例呀。

這個鏈接,我建議你打開看看,這裏面還有 Doug Lea 老爺子的親自解答:

https://bugs.openjdk.java.net/browse/JDK-8062841

首先我們看提出問題的這個人對於問題的描述(可以先不用細看,反正看着也是懵逼的):

通常情況下,被提問的人分為兩類人:

1.遇到過並知道這個問題的人,可以看的明白你在說什麼。

2.雖然沒有碰見過這個問題,但感覺是自己熟悉的領域,可能知道答案,但是看了你的問題描述,也不知道你在說什麼。

這個描述很長,我第一次看的時候很懵逼,很難理解他在說什麼。我就是屬於第二類人。

而且在大多數的問題中,第二類人比第一類人多很多。

但是當我了解到這個 Bug 的來龍去脈的時候,再看這個描述,其實寫的很清楚了,也很好理解。我就變成第一類人了。

但是變成第一類人是有前提的,前提就是我已經了解到了這個地方 Bug 了。可惜,現在是提問,而被提問的人,還對這個 Bug 不是特別了解。

即使,這個被提問的人是 Doug Lea。

可以看到,2014 年 11 月 04 日 Martin 提出這個問題后, Doug Lea 在不到一個小時內就進行了回復,我給大家翻譯一下,老爺子回復的啥:

首先,你說你發現了 ConcurrentHashMap 的問題,但是我沒有看到的測試用例。那麼我就猜測一下是不是有其他線程在計算值的時候被卡住了,但是從你的描述中我也看不到相應的點。

簡單來說就是:Talk is cheap. Show me the code.(屁話少說,放碼過來。)

於是另一個哥們 Pardeep 在一個月後提交了一個測試案例,就是我們前面看到的測試案例:

Pardeep 給 Martin 回復到下面這段話:

他開門見山的說:我注意這個 bug 很長時間了,然後我還有一個測試用例。

可以說這個測試案例的出現,才是真正的轉折點。

然後他提出了自己的看法,這段描述簡短有力的說出了問題的所在(後面我們會講到),然後他還提出了自己的意見。

不到一個小時,這個回到得到了 Doug Lea 的回復:

他說:小伙子的建議還是不錯的,但是現在還不是我們解決這個問題的時候。我們也許會通過代碼改進死鎖檢查機制,以幫助用戶 debug 他們的程序。但是目前而言,這種機制就算做出來,工作效率也是非常低下的,比如在當前的這個案例下。但是現在我們至少清楚的知道,是否要實現這種機制是不能確定的。

總之一句話:問題我知道了,但是目前我還沒想到好的解決方法。

但是,在 19 天以後,老爺子又回來處理這個問題了:

這次的回答可謂是峰迴路轉,他說:請忽略我之前的話。我們發現了一些可行的改進方法,這些改進可以處理更多的用戶錯誤,包括本報告中所提供的測試用例,即解決在 computeIfAbsent 中提供的函數中進行遞歸映射更新導致死鎖這樣的問題。我們會在 JDK 9 裏面解決這個問題。

所以,回顧這個 Bug 被提出的過程。

首先是 Martin 提出了這個問題,並進行了詳細的描述。可惜的是他的描述很專業,是站在你已經了解了這個 Bug 的立場上去描述的,讓人看的很懵逼。

所以 Doug Lea 看到后也表示這啥呀,沒搞懂。

然後是 Pardeep 跟進這個問題,轉折點在於他拋出的這個測試案例。而我相信,既然 Martin 能把這個問題描述的很清楚,他一定是有一個自己的測試案例的,但是他沒有展現出來。

所以,朋友們,測試案例的重要性不言而喻了。問問題的時候不要只是拋出異常,你至少給段對應的代碼,或者日誌,或者一次性描述清楚,寫在文檔裏面發出來也行呀。

Bug 的原因

導致這個 Bug 的原因也是一句話就能說清楚,前面的 Pardeep 老哥也說了:

問題在於我們在進行 computeIfAbsent 的時候,裏面還有一個 computeIfAbsent。而這兩個 computeIfAbsent 它們的 key 對應的 hashCode 是一樣的。

你說巧不巧。

當它們的 hashCode 是一樣的時候,說明它們要往同一個槽放東西。

而當第二個元素進來的時候,發現坑位已經被前一個元素佔領了,可能就是這樣的畫風:

接下來我們就解析一下 computeIfAbsent 方法的工作流程:

第一步是計算 key 對應的 hashCode 應該放到哪個槽裏面。

然後是進入1649 行的這個 for 循環,而這個 for 循環是一個死循環,它在循環體內部判斷各種情況,如果滿足條件則 break 循環。

首先,我們看一下 “AaAa” 和 “BBBB” 經過 spread 計算(右移 16 位高效計算)后的 h 值是什麼:

哇塞,好巧啊,從框起來的這兩部分可以看到,都是 2031775 呢。

說明他們要在同一個槽裏面搞事情。

先是 “AaAa” 進入 computeIfAbsent 方法:

在第一次循環的時候 initTable,沒啥說的。

第二次循環先是在 1653 行計算出數組的下標,並取出該下標的 node。發現這個 node 是空的。於是進入分支判斷:

在標號為 ① 的地方進行 cas 操作,先用 r(即 ReservationNode)進行一個佔位的操作。

在標號為 ② 的地方進行 mappingFunction.apply 的操作,計算 value 值。如果計算出來不為 null,則把 value 組裝成最終的 node。

在標號為 ③ 的東西把之前佔位的 ReservationNode 替換成標號為 ② 的地方組裝成的node 。

問題就出現標號為 ② 的地方。可以看到這裏去進行了 mappingFunction.apply 的操作,而這個操作在我們的案例下,會觸發另一次 computeIfAbsent 操作。

現在 “AaAa” 就等着這個 computeIfAbsent 操作的返回值,然後進行下一步操作,也就是進行標號為 ③ 的操作了。

接着 “BBBB” 就來了。

通過前面我們知道了 “BBBB” 的 hashCode 經過計算后也是和 “AaAa” 一樣。所以它也要想要去那個槽裏面搞事情。

可惜它來晚了一步。

帶大家看一下對應的代碼:

當 key 為 “BBBB” 的時候,算出來的 h 值也是 2031775。

它也會進入 1649 行的這個死循環。然後進行各種判斷。

接下來我要論證的是:

在本文的示例代碼中,當運行到 key 為 “BBBB” 的時候,進入 1649 行這個死循環后,就退不出來了。程序一直在裏面循環運行。

在標號為 ① 的地方,由於這個時候 tab 已經不為 null 了,所以不會進入這個分支。

在標號為 ② 的地方,由於之前 “AaAa” 已經扔了一個 ReservationNode 進去佔位置了,所以不等於 null。所以,也就不會進入這個分支。

怕你懵逼,給你配個圖,真是暖男作者石錘了:

接下來到標號為 ③ 的地方,裏面有一個 MOVED,這個 MOVED 是幹啥的呢?

表示當前的 ConcurrentHashMap 是否是在進行擴容。

很明顯,現在還沒有到該擴容的時候:

第 1678 行的 f 就是之前 “AaAa” 扔進去的 ReservationNode ,這個 Node 的 hash 是 -3,不等於MOVED(-1)。

所以,不會進入這個分支判斷。

接下來,能進的只有標號為 ④ 的地方了,所以我們只需要把這個地方攻破,就徹底了解這個 Bug 了。

走起:

通過前面的分析我們知道了,當前案例情況下,只會進入 1672 行這個分支。

而這個分支裏面,還有四個判斷。我們一個個的攻破:

標號為 ⑤ 的地方,tabAt 方法取出來的對象,就是之前 “AaAa” 放進去的佔位的 ReservationNode ,也就是這個 f 。所以可以進入這個分支判斷。

標號為 ⑥ 的地方,fh >=0 。而 fh 是當前 node 的 hash 值,大於 0 說明當前是按照鏈表存儲的數據。之前我們分析過了,當前的 hash 值是 -3。所以,不會進入這個分支。

標號為 ⑦ 的地方,判斷 f 節點是否是紅黑樹存儲。當然不是的。所以,不會進入這個分支。

標號為 ⑧ 的地方,binCount 代表的是該下標裏面,有幾個 node 節點。很明顯,現在一個都沒有。所以當前的 binCount 還是 0 。所以,不會進入這個分支。

完了。分析完了。

Bug 也就出來了,一次 for 循環結束后,沒有 break。苦就苦在這個 for 循環還是個死循環。

再來一個上帝視角,看看當 key 為 “BBBB” 的時候發生了什麼事情:

進入無限循環內:

①.經過 “AaAa” 之後,tab 就不為 null 了。

②.當前的槽中已經被 “AaAa” 先放了一個 ReservationNode 進行佔位了,所以不為 null。

③.當前的 map 並沒有進行擴容操作。

④.包含⑤、⑥、⑦、⑧。

⑤.tabAt 方法取出來的對象,就是之前 “AaAa” 放進去的佔位的 ReservationNode,所以滿足條件進入分支。

⑥.判斷當前是否是鏈表存儲,不滿足條件,跳過。

⑦.判斷當前是否是紅黑樹存儲,不滿足條件,跳過。

⑧.判斷當前下標裏面是否放了 node,不滿足條件(“AaAa” 只有個佔位的Node ,並沒有初始完成,所以還沒有放到該下標裏面),進入下一次循環。

然後它就在死循環裏面出不來了!

我相信現在大家對於這個 Bug 的來路了解清楚了。

如果你是在 idea 裏面跑這個測試用例,也可以這樣直觀的看一眼:

點擊這個照相機圖標:

從線程快照裏面其實也是可以看到端倪的,大家可以去分析分析。

有的觀點說的是由於線程安全的導致的死循環,經過分析我覺得這個觀點是不對的。

它存在死循環,不是由於線程安全導致的,純粹是自己進入了死循環。

或者說,這是一個“彩蛋”?

或者……自信點,就說這事 Bug ,能穩定復現的那種。

那麼我們如果是使用 JDK 8 怎麼避免踩到這個“彩蛋”呢?

看看 Dubbo 裏面是怎麼解決的:

先調用了 get 方法,如果返回為 null,則調用 putIfAbsent 方法,這樣就能實現和之前一樣的效果了。

如果你在項目中也有使用 computeIfAbsent 的地方,建議也這樣去修改。

說到 ConcurrentHashMap get 方法返回 null,我就想起了之前討論的一個面試題了:

答案都寫在這個文章裏面了,有興趣的可以了解一下《這道面試題我真不知道面試官想要的回答是什麼》

Bug 的解決 其實徹底理解了這個 Bug 之後,我們再來看一下 JDK 9 裏面的解決方案,看一下官方源碼對比:

http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/main/java/util/concurrent/ConcurrentHashMap.java?r1=1.258&r2=1.259&sortby=date&diff_format=f

就加了兩行代碼,判斷完是否是紅黑樹節點后,再判斷一下是否是 ReservationNode 節點,因為這個節點就是個佔位節點。如果是,則拋出異常。

就這麼簡單。沒有什麼神秘的。

所以,如果你在 JDK 9 裏面執行文本的測試用例,就會拋出 IllegalStateException。

這就是 Doug Lea 之前提到的解決方案:

了解了這個 Bug 的來龍去脈后,特別是看到解決方案后,我們就能輕描淡寫的說一句:

害,就這?沒聽說過!

另外,我看 JDK 9 修復的時候還不止修復了一個問題:

http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/6dd59c01f011/src/java.base/share/classes/java/util/concurrent/ConcurrentHashMap.java

你去翻一翻。發現,啊,全是知識點啊,學不動了。

釣魚執法

為什麼我在文章的一開始就說了這是 Doug Lea 在釣魚執法呢?

因為在最開始提問的藝術那一部分,我相信,Doug Lea 跑完那個測試案例之後,心裏也有點數了。

大概知道問題在哪了,而且從他的回答和他寫的文檔中我也有理由相信,他寫的這個方法的時候就知道可能會出問題。

而且,Pardeep 的回復中提到了文檔,那我們就去看看官方文檔對於該方法的描述是怎樣的:

https://docs.oracle.com/javase/8/docs/api/

文檔中說函數方法應該簡短,簡單。而且不能在更新的映射的時候更新映射。就是說不能套娃。

套娃,用程序說就是recursive(遞歸),按照文檔說如果存在遞歸,則會拋出 IllegalStateException 。

而提到遞歸,你想到了什麼?

我首先就想到了斐波拉契函數。我們用 computeIfAbsent 實現一個斐波拉契函數如下:

public class Test {

static Map<Integer, Integer> cache = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        System.out.println("f(" + 14 + ") =" + fibonacci(14));
    }

    static int fibonacci(int i) {
        if (i == 0)
            return i;
        if (i == 1)
            return 1;
        return cache.computeIfAbsent(i, (key) -> {
            System.out.println("Slow calculation of " + key);
            return fibonacci(i - 2) + fibonacci(i - 1);
        });
    }
}

這就是遞歸調用,我用 JDK 1.8 跑的時候並沒有拋出 IllegalStateException,只是程序假死了,原因和我們前面分析的是一樣一樣的。我理解這個地方是和文檔不符的。

所以,我懷疑是 Doug Lea 在這個地方釣魚執法。

CHM一定線程安全嗎?

既然都說到 currentHashMap(CHM)了,那我說一個相關的注意點吧。

首先 CHM 一定能保證線程安全嗎?

是的,CHM 本身一定是線程安全的。但是,如果你使用不當還是有可能會出現線程不安全的情況。

給大家看一點 Spring 中的源碼吧:

org.springframework.core.SimpleAliasRegistry

在這個類中,aliasMap 是 ConcurrentHashMap 類型的:

在 registerAlias 和 getAliases 方法中,都有對 aliasMap 進行操作的代碼,但是在操作之前都是用 synchronized 把 aliasMap 鎖住了。

為什麼?為什麼我們操作 ConcurrentHashMap 的時候還要加鎖呢?

這個是根據場景而定的,這個別名管理器,在這裏加鎖應該是為了避免多個線程操作 ConcurrentHashMap 。

雖然 ConcurrentHashMap 是線程安全的,但是假設如果一個線程 put,一個線程 get,在這個代碼的場景裏面是不允許的。

如果覺得不太好理解的話我舉一個 redis 的例子。

redis 的 get、set 方法都是線程安全的吧。但是你如果先 get 再 set,那麼在多線程的情況下還是會有問題的。

因為這兩個操作不是原子性的。所以 incr 就應運而生了。

我舉這個例子的是想說線程安全與否不是絕對的,要看場景。給你一個線程安全的容器,你使用不當還是會有線程安全的問題。

再比如,HashMap 一定是線程不安全的嗎?

說不能說的這麼死吧。它是一個線程不安全的容器。但是如果我的使用場景是只讀呢?

在這個只讀的場景下,它就是線程安全的。

總之,看場景。道理,就是這麼一個道理。

最後說兩句(求關注)

所以點個“贊”吧,周更很累的,不要白嫖我,需要一點正反饋。

才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言指出來,我對其加以修改。

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

我是 why,一個被代碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。

歡迎關注我的微信公眾號:why技術。在這裏我會分享一些java技術相關的知識,用匠心敲代碼,對每一行代碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評、影評。感謝你的關注,願你我共同進步。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

結合中斷上下文切換和進程上下文切換分析Linux內核的一般執行過程

結合中斷上下文切換和進程上下文切換分析Linux內核的一般執行過程

目錄

  • 結合中斷上下文切換和進程上下文切換分析Linux內核的一般執行過程
    • 一. 實驗準備
    • 二. 實驗過程
      • I 分析中斷上下文的切換
      • II 分析fork子進程啟動執行時進程上下文及其特殊之處
      • III 分析execve系統調用中斷上下文及其特殊之處
      • IV 以系統調用作為特殊的中斷,結合中斷上下文切換和進程上下文切換分析Linux系統的一般執行過程
    • 三. 總結

一. 實驗準備

  1. 詳細要求

結合中斷上下文切換和進程上下文切換分析Linux內核一般執行過程

  • 以fork和execve系統調用為例分析中斷上下文的切換
  • 分析execve系統調用中斷上下文的特殊之處
  • 分析fork子進程啟動執行時進程上下文的特殊之處
  • 以系統調用作為特殊的中斷,結合中斷上下文切換和進程上下文切換分析Linux系統的一般執行過程

完成一篇博客總結分析Linux系統的一般執行過程,以期對Linux系統的整體運作形成一套邏輯自洽的模型,並能將所學的各種OS和Linux內核知識/原理融通進模型中

  1. 實驗環境

發行版本:Ubuntu 18.04.4 LTS

處理器:Intel® Core™ i7-8850H CPU @ 2.60GHz × 3

圖形卡:Parallels using AMD® Radeon pro 560x opengl engine

GNOME:3.28.2

二. 實驗過程

I 分析中斷上下文的切換

中斷髮生以後,CPU跳到內核設置好的中斷處理代碼中去,由這部分內核代碼來處理中斷。這個處理過程中的上下文就是中斷上下文

幾乎所有的體繫結構,都提供了中斷機制。當硬件設備想和系統通信的時候,它首先發出一個異步的中斷信號去打斷處理器的執行,繼而打斷內核的執行。中斷通常對應着一个中斷號,內核通過這个中斷號找到中斷服務程序,調用這個程序響應和處理中斷。當你敲擊鍵盤時,鍵盤控制器發送一个中斷信號告知系統,鍵盤緩衝區有數據到來,內核收到這个中斷號,調用相應的中斷服務程序,該服務程序處理鍵盤數據然後通知鍵盤控制器可以繼續輸入數據了。為了保證同步,內核可以使用中止—既可以停止所有的中斷也可以有選擇地停止某个中斷號對應的中斷,許多操作系統的中斷服務程序都不在進程上下文中執行,它們在一個與所有進程無關的、專門的中斷上下文中執行。之所以存在這樣一個專門的執行環境,為了保證中斷服務程序能夠在第一時間響應和處理中斷請求,然後快速退出。

對同一個CPU來說,中斷處理比進程擁有更高的優先級,所以中斷上下文切換並不會與進程上下文切換同時發生。由於中斷程序會打斷正常進程的調度和運行,大部分中斷處理程序都短小精悍,以便盡可能快的執行結束。

一個進程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統級上下文。

用戶級上下文: 正文、數據、用戶堆棧以及共享存儲區;
寄存器上下文: 通用寄存器、程序寄存器(IP)、處理器狀態寄存器(EFLAGS)、棧指針(ESP);
系統級上下文: 進程控制塊task_struct、內存管理信息(mm_struct、vm_area_struct、pgd、pte)、內核棧。

當發生進程調度時,進行進程切換就是上下文切換(context switch)。操作系統必須對上面提到的全部信息進行切換,新調度的進程才能運行。而系統調用進行的是模式切換(mode switch)。模式切換與進程切換比較起來,容易很多,而且節省時間,因為模式切換最主要的任務只是切換進程寄存器上下文的切換。

II 分析fork子進程啟動執行時進程上下文及其特殊之處

fork()系統調用會通過複製一個現有進程來創建一個全新的進程. 進程被存放在一個叫做任務隊列的雙向循環鏈表當中。鏈表當中的每一項都是類型為task_struct成為進程描述符的結構。

首先我們來看一段代碼

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
  pid_t pid;
  char *message;
  int n;
  pid = fork();
  if(pid<0){
    perror("fork failed");
    exit(1);
  }
  if (pid == 0){
    message = "this is the child \n";
    n=6;
  }else {
    message = "this is the parent \n";
    n=3;
  }
  for(;n>0;n--){
    printf("%s",message);
    sleep(1);
  }
  return 0;
}

在Linux環境中編寫和執行

# 創建一個C文件,名為t.c,將上面的代碼拷貝進去
touch t.c
# 進行編譯
gcc t.c
# 執行
./a.out

之所以輸出是這樣的結果,是因為程序的執行流程如下圖所示:

以上的fork()例子的執行流程大致如下:

  1. 父進程初始化。
  2. 父進程調用fork,這是一個系統調用,因此進入內核。
  3. 內核根據父進程複製出一個子進程,父進程和子進程的PCB信息相同,用戶態代碼和數據也相同。因此,子進程現在的狀態看起來和父進程一樣,做完了初始化,剛調用了fork進入內核,還沒有從內核返回。
  4. 現在有兩個一模一樣的進程看起來都調用了fork進入內核等待從內核返回(實際上fork只調用了一次),此外系統中還有很多別的進程也等待從內核返回。是父進程先返回還是子進程先返回,還是這兩個進程都等待,先去調度執行別的進程,這都不一定,取決於內核的調度算法。
  5. 如果某個時刻父進程被調度執行了,從內核返回后就從fork函數返回,保存在變量pid中的返回值是子進程的id,是一個大於0的整數,因此執下面的else分支,然後執行for循環,打印"This is the parent\n"三次之後終止。
  6. 如果某個時刻子進程被調度執行了,從內核返回后就從fork函數返回,保存在變量pid中的返回值是0,因此執行下面的if (pid == 0)分支,然後執行for循環,打印"This is the child\n"六次之後終止。fork調用把父進程的數據複製一份給子進程,但此後二者互不影響,在這個例子中,fork調用之後父進程和子進程的變量messagen被賦予不同的值,互不影響。
  7. 父進程每打印一條消息就睡眠1秒,這時內核調度別的進程執行,在1秒這麼長的間隙里(對於計算機來說1秒很長了)子進程很有可能被調度到。同樣地,子進程每打印一條消息就睡眠1秒,在這1秒期間父進程也很有可能被調度到。所以程序運行的結果基本上是父子進程交替打印,但這也不是一定的,取決於系統中其它進程的運行情況和內核的調度算法,如果系統中其它進程非常繁忙則有可能觀察到不同的結果。另外,讀者也可以把sleep(1);去掉看程序的運行結果如何。
  8. 這個程序是在Shell下運行的,因此Shell進程是父進程的父進程。父進程運行時Shell進程處於等待狀態,當父進程終止時Shell進程認為命令執行結束了,於是打印Shell提示符,而事實上子進程這時還沒結束,所以子進程的消息打印到了Shell提示符後面。最後光標停在This is the child的下一行,這時用戶仍然可以敲命令,即使命令不是緊跟在提示符後面,Shell也能正確讀取。

fork()最特殊之處在於:成功調用后返回兩個值,是由於在複製時複製了父進程的堆棧段,所以兩個進程都停留在fork函數中,等待返回。所以fork函數會返回兩次,一次是在父進程中返回,另一次是在子進程中返回,這兩次的返回值不同

其中父進程返回子進程pid,這是由於一個進程可以有多個子進程,但是卻沒有一個函數可以讓一個進程來獲得這些子進程id,那談何給別人你創建出來的進程。而子進程返回0,這是由於子進程可以調用getppid獲得其父進程進程ID,但這個父進程ID卻不可能為0,因為進程ID0總是有內核交換進程所用,故返回0就可代表正常返回了。

從fork函數開始以後的代碼父子共享,既父進程要執行這段代碼,子進程也要執行這段代碼.(子進程獲得父進程數據空間,堆和棧的副本. 但是父子進程並不共享這些存儲空間部分. (即父,子進程共享代碼段.)。現在很多實現並不執行一個父進程數據段,堆和棧的完全複製. 而是採用寫時拷貝技術。這些區域有父子進程共享,而且內核地他們的訪問權限改為只讀的.如果父子進程中任一個試圖修改這些區域,則內核值為修改區域的那塊內存製作一個副本, 也就是如果你不修改我們一起用,你修改了之後對於修改的那部分內容我們分開各用個的。

再一個就是,在重定向父進程的標準輸出時,子進程標準輸出也被重定向。這就源於父子進程會共享所有的打開文件。 因為fork的特性就是將父進程所有打開文件描述符複製到子進程中。當父進程的標準輸出被重定向,子進程本是寫到標準輸出的時候,此時自然也改寫到那個對應的地方;與此同時,在父進程等待子進程執行時,子進程被改寫到文件show.out中,然後又更新了與父進程共享的該文件的偏移量;那麼在子進程終止后,父進程也寫到show.out中,同時其輸出還會追加在子進程所寫數據之後。

在fork之後處理文件描述符一般有以下兩種情況:

  • 父進程等待子進程完成。此種情況,父進程無需對其描述符作任何處理。當子進程終止后,它曾進行過讀,寫操作的任一共享描述符的文件偏移已發生改變。
  • 父子進程各自執行不同的程序段。這樣fork之後,父進程和子進程各自關閉它們不再使用的文件描述符,這樣就避免干擾對方使用的文件描述符了。這類似於網絡服務進程。

同時父子進程也是有區別的:它們不僅僅是兩個返回值不同;它們各自的父進程也不同,父進程的父進程是ID不變的;還有子進程不繼承父進程設置的文件鎖,子進程未處理的信號集會設置為空集等不同

事實上linux平台通過clone()系統調用實現fork()。fork(),vfork()和clone()庫函數都根據各自需要的參數標誌去調用clone(),然後由clone()去調用do_fork(). 再然後do_fork()完成了創建中的大部分工作,他定義在kernel/fork.c當中.該函數調用copy_process()。

具體的流程可以參考下圖:

III 分析execve系統調用中斷上下文及其特殊之處

execve() 系統調用的作用是運行另外一個指定的程序。它會把新程序加載到當前進程的內存空間內,當前的進程會被丟棄,它的堆、棧和所有的段數據都會被新進程相應的部分代替,然後會從新程序的初始化代碼和 main 函數開始運行。同時,進程的 ID 將保持不變。

execve() 系統調用通常與 fork() 系統調用配合使用。從一個進程中啟動另一個程序時,通常是先 fork() 一個子進程,然後在子進程中使用 execve() 變身為運行指定程序的進程。 例如,當用戶在 Shell 下輸入一條命令啟動指定程序時,Shell 就是先 fork() 了自身進程,然後在子進程中使用 execve() 來運行指定的程序。

Linux提供了execl、execlp、execle、execv、execvp和execve等六個用以執行一個可執行文件的函數(統稱為exec函數,其間的差異在於對命令行參數和環境變量參數的傳遞方式不同)。這些函數的第一個參數都是要被執行的程序的路徑,第二個參數則向程序傳遞了命令行參數,第三個參數則向程序傳遞環境變量。以上函數的本質都是調用在arch/i386/kernel/process.c文件中實現的系統調用sys_execve來執行一個可執行文件。

asmlinkage int sys_execve(struct pt_regs regs)
{
    int  error;
    char * filename;
    //將可執行文件的名稱裝入到一個新分配的頁面中
    filename = getname((char __user *) regs.ebx);
    error = PTR_ERR(filename);
    if (IS_ERR(filename))
       goto out;
    //執行可執行文件
    error = do_execve(filename,
          (char __user * __user *) regs.ecx,
          (char __user * __user *) regs.edx,
         &regs);
    if (error == 0) {
       task_lock(current);
       current->ptrace &= ~PT_DTRACE;
       task_unlock(current);
       
       set_thread_flag(TIF_IRET);
    }
    putname(filename);
out:
    return error;
}

該系統調用所需要的參數pt_regs在include/asm-i386/ptrace.h文件中定義。該參數描述了在執行該系統調用時,用戶態下的CPU寄存器在核心態的棧中的保存情況。通過這個參數,sys_execve可以獲得保存在用戶空間的以下信息:可執行文件路徑的指針(regs.ebx中)、命令行參數的指針(regs.ecx中)和環境變量的指針(regs.edx中)。

struct pt_regs {
    long ebx;
    long ecx;
    long edx;
    long esi;
    long edi;
    long ebp;
    long eax;
    int xds;
    int xes;
    long orig_eax;
    long eip;
    int xcs;
    long eflags;
    long esp;
    int xss;
};

regs.ebx保存着系統調用execve的第一個參數,即可執行文件的路徑名。因為路徑名存儲在用戶空間中,這裏要通過getname拷貝到內核空間中。getname在拷貝文件名時,先申請了一個page作為緩衝,然後再從用戶空間拷貝字符串。為什麼要申請一個頁面而不使用進程的系統空間堆棧?首先這是一個絕對路徑名,可能比較長,其次進程的系統空間堆棧大約為7K,比較緊缺,不宜濫用。用完文件名后,在函數的末尾調用putname釋放掉申請的那個頁面。

sys_execve的核心是調用do_execve函數,傳給do_execve的第一個參數是已經拷貝到內核空間的路徑名filename,第二個和第三個參數仍然是系統調用execve的第二個參數argv和第三個參數envp,它們代表的傳給可執行文件的參數和環境變量仍然保留在用戶空間中。簡單分析一下這個函數的思路:先通過open_err()函數找到並打開可執行文件,然後要從打開的文件中將可執行文件的信息裝入一個數據結構linux_binprm,do_execve先對參數和環境變量的技術,並通過prepare_binprm讀入開頭的128個字節到linux_binprm結構的bprm緩衝區,最後將執行的參數從用戶空間拷貝到數據結構bprm中。內核中有一個formats隊列,該隊列的每個成員認識並只處理一種格式的可執行文件,bprm緩衝區中的128個字節中有格式信息,便要通過這個隊列去辨認。do_execve()中的關鍵是最後執行一個search_binary_handler()函數,找到對應的執行文件格式,並返回一個值,這樣程序就可以執行了。

do_execve 定義在 <fs/exec.c> 中,關鍵代碼解析如下。

int do_execve(char * filename, char __user *__user *argv,
       char __user *__user *envp,    struct pt_regs * regs)
{
    struct linux_binprm *bprm; //保存要執行的文件相關的數據
    struct file *file;
    int retval;
    int i;
    retval = -ENOMEM;
    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
    if (!bprm)
       goto out_ret;
    //打開要執行的文件,並檢查其有效性(這裏的檢查並不完備)
    file = open_exec(filename);
    retval = PTR_ERR(file);
    if (IS_ERR(file))
       goto out_kfree;
    //在多處理器系統中才執行,用以分配負載最低的CPU來執行新程序
    //該函數在include/linux/sched.h文件中被定義如下:
    // #ifdef CONFIG_SMP
    // extern void sched_exec(void);
    // #else
    // #define sched_exec() {}
    // #endif
    sched_exec();
    //填充linux_binprm結構
    bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
    bprm->file = file;
    bprm->filename = filename;
    bprm->interp = filename;
    bprm->mm = mm_alloc();
    retval = -ENOMEM;
    if (!bprm->mm)
       goto out_file;
    //檢查當前進程是否在使用LDT,如果是則給新進程分配一個LDT
    retval = init_new_context(current, bprm->mm);
    if (retval  0)
       goto out_mm;
    //繼續填充linux_binprm結構
    bprm->argc = count(argv, bprm->p / sizeof(void *));
    if ((retval = bprm->argc)  0)
       goto out_mm;
    bprm->envc = count(envp, bprm->p / sizeof(void *));
    if ((retval = bprm->envc)  0)
       goto out_mm;
    retval = security_bprm_alloc(bprm);
    if (retval)
       goto out;
    //檢查文件是否可以被執行,填充linux_binprm結構中的e_uid和e_gid項
    //使用可執行文件的前128個字節來填充linux_binprm結構中的buf項
    retval = prepare_binprm(bprm);
    if (retval  0)
       goto out;
    //將文件名、環境變量和命令行參數拷貝到新分配的頁面中
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    if (retval  0)
       goto out;
    bprm->exec = bprm->p;
    retval = copy_strings(bprm->envc, envp, bprm);
    if (retval  0)
       goto out;
    retval = copy_strings(bprm->argc, argv, bprm);
    if (retval  0)
       goto out;
    //查詢能夠處理該可執行文件格式的處理函數,並調用相應的load_library方法進行處理
    retval = search_binary_handler(bprm,regs);
    if (retval >= 0) {
       free_arg_pages(bprm);
       //執行成功
       security_bprm_free(bprm);
       acct_update_integrals(current);
       kfree(bprm);
       return retval;
    }
out:
    //發生錯誤,返回inode,並釋放資源
    for (i = 0 ; i  MAX_ARG_PAGES ; i++) {
       struct page * page = bprm->page;
       if (page)
         __free_page(page);
    }
    if (bprm->security)
       security_bprm_free(bprm);
out_mm:
    if (bprm->mm)
       mmdrop(bprm->mm);
out_file:
    if (bprm->file) {
       allow_write_access(bprm->file);
       fput(bprm->file);
    }
out_kfree:
    kfree(bprm);
out_ret:
    return retval;
}

該函數用到了一個類型為linux_binprm的結構體來保存要執行的文件相關的信息,該結構體在include/linux/binfmts.h文件中定義:

struct linux_binprm{
    char buf[BINPRM_BUF_SIZE]; //保存可執行文件的頭128字節
    struct page *page[MAX_ARG_PAGES];
    struct mm_struct *mm;
    unsigned long p;    //當前內存頁最高地址
    int sh_bang;
    struct file * file;     //要執行的文件
    int e_uid, e_gid;    //要執行的進程的有效用戶ID和有效組ID
    kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
    void *security;
    int argc, envc;     //命令行參數和環境變量數目
    char * filename;   //要執行的文件的名稱
    char * interp;       //要執行的文件的真實名稱,通常和filename相同
   unsigned interp_flags;
    unsigned interp_data;
    unsigned long loader, exec;
};

在該函數的最後,又調用了fs/exec.c文件中定義的search_binary_handler函數來查詢能夠處理相應可執行文件格式的處理器,並調用相應的load_library方法以啟動進程。這裏,用到了一個在include/linux/binfmts.h文件中定義的linux_binfmt結構體來保存處理相應格式的可執行文件的函數指針如下:

struct linux_binfmt {
    struct linux_binfmt * next;
    struct module *module;
    // 加載一個新的進程
    int (*load_binary)(struct linux_binprm *, struct pt_regs * regs);
    // 動態加載共享庫
    int (*load_shlib)(struct file *);
    // 將當前進程的上下文保存在一個名為core的文件中
   int (*core_dump)(long signr, struct pt_regs * regs, struct file * file);
    unsigned long min_coredump;
};

Linux內核允許用戶通過調用在include/linux/binfmt.h文件中定義的register_binfmt和unregister_binfmt函數來添加和刪除linux_binfmt結構體鏈表中的元素,以支持用戶特定的可執行文件類型。
在調用特定的load_binary函數加載一定格式的可執行文件后,程序將返回到sys_execve函數中繼續執行。該函數在完成最後幾步的清理工作后,將會結束處理並返回到用戶態中,最後,系統將會將CPU分配給新加載的程序。

execve系統調用的過程總結如下:

  • execve系統調用陷入內核,並傳入命令行參數和shell上下文環境
  • execve陷入內核的第一個函數:do_execve,該函數封裝命令行參數和shell上下文
  • do_execve調用do_execveat_common,後者進一步調用__do_execve_file,打開ELF文件並把所有的信息一股腦的裝入linux_binprm結構體
  • do_execve_file中調用search_binary_handler,尋找解析ELF文件的函數
  • search_binary_handler找到ELF文件解析函數load_elf_binary
  • load_elf_binary解析ELF文件,把ELF文件裝入內存,修改進程的用戶態堆棧(主要是把命令行參數和shell上下文加入到用戶態堆棧),修改進程的數據段代碼段
  • load_elf_binary調用start_thread修改進程內核堆棧(特別是內核堆棧的ip指針)
  • 進程從execve返回到用戶態后ip指向ELF文件的main函數地址,用戶態堆棧中包含了命令行參數和shell上下文環境

IV 以系統調用作為特殊的中斷,結合中斷上下文切換和進程上下文切換分析Linux系統的一般執行過程

Linux系統的一般執行過程

正在運行的用戶態進程X切換到運行用戶態進程Y的過程

  1. 發生中斷 ,完成以下步驟:

    save cs:eip/esp/eflags(current) to kernel stack
    load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack)

  2. SAVE_ALL //保存現場,這裡是已經進入內核中斷處里過程

  3. 中斷處理過程中或中斷返回前調用了schedule(),其中的switch_to做了關鍵的進程上下文切換

  4. 標號1之後開始運行用戶態進程Y(這裏Y曾經通過以上步驟被切換出去過因此可以從標號1繼續執行)

  5. restore_all //恢復現場

  6. 繼續運行用戶態進程Y

進程間的特殊情況

  • 通過中斷處理過程中的調度時機,用戶態進程與內核線程之間互相切換和內核線程之間互相切換
  • 與最一般的情況非常類似,只是內核線程運行過程中發生中斷沒有進程用戶態和內核態的轉換;
  • 內核線程主動調用schedule(),只有進程上下文的切換,沒有發生中斷上下文的切換,與最一般“的情況略簡略;
  • 創建子進程的系統調用在子進程中的執行起點及返回用戶態,如fork;
  • 加載一個新的可執行程序后返回到用戶態的情況,如execve;0-3G內核態和用戶態都可以訪問,3G以上只能內核態訪問。內核是所有進程共享的。內核是各種中斷處理過程和內核線程的集合。

三. 總結

這次實驗主要做了如下的事情:

  • 學習並完成實驗環境的配置的搭建
  • 學習並了解Linux內核中系統調用相關知識
  • 學習了中斷相關的知識
  • 學習並實踐了fork()與execve()系統調用的知識
  • 思考代碼執行的流程與原理

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

敏捷開發:最近的收穫和站會上的小黃鴨

最近

在博客園經常分享一些心得,有幸在另一個平台做了一場直播。

自己精心準備了很長時間,從素材和文章以及一字一句。

過程中感覺自信滿滿,後期再去回顧發現其實硬貨還需要再硬一點。

dxxxxxx 平台的分享

自己在這個社區進行分享的時候,開始自己照着自己的演講稿念,沒多久自己發現根據演講稿年思路不夠連貫。

自己扔掉演講稿根據之前自己準備的主體脈絡進行分享,有準備但是還是覺得臨場發揮思路更加連貫和清晰。

一個半小時過去了,我的分享完畢,這種直播的壓力確實是比較大的,因為你要直接面對聽眾,會遇到一些突發事情。

期間就發生了2次離線,還有你要關注評論的同時也不能讓評論影響你的節奏,

對於一些重要的評論在分享過程中隨時回復還是記錄下來?

直播結束后,第二天我又重新聽了一下自己的分享,總結一下直播的經驗。

過程比結果收穫更大。期間還有一件事讓我很是糾結。

我發朋友圈做宣傳,把自己的同事給屏蔽了,因為當時的我沒有自信,怕他們笑話。

心中的那種不自信的感覺完全戰勝了一切。自己不是技術最牛的,自己也不是這個管理經驗最豐富的。

我對自己這種糾結懊悔了很久,正常地做自己就好了,又不是做給別人看的。

下次再有機會我會更加自信,即便不是最專業,那也需要一份對自己努力的自信。

另一個話題

沒有一絲防備,我們直接切換另一個話題。

上面算是給自己這3個月沒寫博客劃一個句號。我們來看看最近我們在敏捷一些什麼?

我們項目組組織了讀書分享,當時組織這個讀書分享我也是很糾結。

作為項目經理不是職能經理,自己到底組織一場讀書分享合不合適?

我把我的疑惑和需求說給了我們項目群經理,希望從他那裡能夠得到一些經驗。

疑惑:我們從項目角度出發來組織學習,會不會觸碰了職能經理的職能邊界?

(比如讓大家閱讀《Scrum精髓:敏捷轉型指南》,會不會觸碰產品職能的邊界?)

需求:我希望大家能夠系統地認識敏捷和框架。
從中能夠學習到一些東西來應用到項目中,其次大家對於這種項目內的分享形式是擁抱的態度

經理的建議是可以嘗試一下。我們項目組開了一個會,約定了一下。

在不佔用大家太多的時間,我們挑選值得閱讀的章節么一個項目中帶着一次分享,每次分享大約1-2個章節。

是的,我們邁出了第一步

我們其實是在項目結束后一周內,確定好閱讀範圍,確定好分享人,確定好分享時長兩小時。

流程分為依次提問和自由提問,分享人可以拿任何的分享資料分享(doc,表格)都行。

目的就是讓大家心裏有一定的概念並對當前最容易實行的一個敏捷方法進行深入討論。

這是當時分享人的一個分享大綱

第一次分享,大家也在適應遮掩的一種分享狀態。

整體過程很順利也夠味,如果能夠再放鬆一些那就更好了,大家說著說著就說成了項目總結會~

從這次會議中的一些啟發

從有了開這種分享會的想法到實行,總結下來就是如果大家都抱有期待並且準備充足那就先試試。有想法就嘗試。

一些之前的想去執行的想法,當下就會執行嘗試。

站會上的小鴨子

終於到了標題中的這一部分。之前讀敏捷相關的書籍時,很多會提到站會上需要一個像權力交接棒的實際物品來標識你的權力~

以前我們開站會,時間和習慣都不錯,到時間我提醒大家開會,大家湊一堆開始開會。

但是,總覺得缺少一些什麼讓整個會議顯得有點太過於形式。

我想起了開始的那句話:有一個實際的權力物品來做交接。

看吧,就是這隻小鴨子

改變

每天的站會,不同的主持人,主持人的象徵就是擁有這隻小鴨子~

就是誰拿到這隻小鴨子,誰就是站會的負責人和支持人。

8:40 他會在群里喊,然後會議上主持,結束后流動到另一位組織者手裡~

大家對站會接受度更高了,對這個項目也有了一些感情,團隊的人的默契和氛圍會默默提高很多。

這種感覺是潛移默化的,也是需要我們隨時提醒和建立的。

再回過頭來說那次敏捷分享的效果,我們之前是開發,然後測試,最後上線。

我們雖然是分階段提測但是沒有執行分階段測試。

所以這次新版本中我們“打成”一致嘗試分階段提測後分階段測試。就是 開發-測試交替進行。

有沒有難度呢?有,就是大家的時間和專註度會受到衝擊。

對於實踐這個開發、測試交替進行我們承認一些東西也相信一些東西。

我們組內也承認起初實踐起來肯定會比之前的模式有一定的不適應,但是我們依然信心。

希望可以踴躍暴漏問題不管是個人還是團隊不管是心情還是問題,都暴露出來。

我們組內達成了一致默默嘗試了一下,我們重視了組內的衝刺總結會,其實從總結會上我們收穫還是很多的。

一些開發和溝通問題,組內他們能夠自組織去主動解決,根據目前組內的主動性,我覺得我不需要關心。

我重點解決了一下時間和專註度衝突的問題。

開發正在開發下一個階段,測試正在測試你上一個階段,突然一個bug給你你煩不煩?開不開心?

–你是馬上斷開思路去改正?還是抱着忐忑的心情繼續開發?

開發修復完bug,馬上回給測試,測試此時正在測試下一個階段,

–你是斷開思路馬上去回歸?還是思索着會不會影響後面的功能的心情測試下去?

達成一致

我們就問題討論,最終達成一致

當然我們還依然堅信,剛開始實踐期間肯定會有不適應和一些問題。

但是我們還堅持要解決問題並把問題的根因找出來,想辦法解決。這是毋庸置疑的。

分階段提測,大家達成一致是因為每一次提交的功能不是很多,測試測試之前可以先在群里發出通知。

開發就一些工作進行微調,便於後面的bug修改。

測試發通知–3:00統一提bug–開發4:00開始修復–測試最晚命題談進行回歸

還有一點就是,對於流程和嚴重bug,隨時支持,沒有理由。這個大家也是認可的。

總結

其實這一段時間對於我到其他社區進行了一次敏捷分享;

對於項目組,我們使用權力交接棒–小黃鴨,進行大家主人翁意識的交接培養。

對於項目,我們實踐衝刺,我們開始重視每一次的衝刺總結會,

我們希望團隊中任何一個人都可以發現問題並自己發起會議(當然也是集中遇到問題或者集中解決問題)

我們實踐開發-測試交替進行的模式,為什麼呢?

這個原因我們的測試總結得很好,縮短時間其次,重要的是測試可以提前參与,提前發現問題,而不是最後採取發現問題。

我理解的是通過測試的力量在過程中來保駕護航,而不是在項目最後去修修補補。

最近感觸很大,當一些行動獲得大家認可的時候,大家互相能理解,大家也都互相提建議。

為了讓項目更好,讓我們更加自主,你好,我好,項目好。你提高,我提高,項目也提高。

當前的應屆生的學習能力和融合能力確實很強。

作為項目經理除了要保證項目交付這個基本目標以外,也是希望大家從項目中能夠獲得一些東西。我更希望是自驅的動力~

如果可以,可以把項目中的好的實踐帶到其他項目中去,去默默影響他人。

堅持自己,堅持自己的那些原則。獲得好的優秀的經驗,慢慢影響他人。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

防疫封鎖 馬達加斯加狐猴族群獲得喘息

摘錄自2020年9月24日公視報導

安達西貝自然保護區是馬達加斯加島生態觀光的熱門地點之一,過去五個月因為新冠病毒疫情造成的封鎖以及邊境管制,當地最具代表性的動物狐猴,在原始雨林裡享受了沒有人類嘈雜群聚,寧靜自得的生活。島內九月起放寬了封鎖政策,觀光人數開始緩步回升,但因為國際航班仍然停擺,多數是馬達加斯加人自己的島內旅遊。

3月起陷入封鎖寒冬的旅遊業者,樂見顧客回歸,但富裕的外國觀光客進不來,仍然入不敷出。經濟壓力也傷害了自然環境。森林嚮導發現,最近林木被濫伐破壞的情況比過去30年都要嚴重。

馬達加斯加的旅遊業佔全國經濟的7%,堪稱重要支柱,而雨林生態體系脆弱,全國上百種的狐猴,幾乎都已經因為盜獵、暖化與森林砍伐,名列國際自然保護聯盟的紅色瀕危名單,其中30種更已經是極度瀕危,離滅絕只差一步。

生物多樣性
國際新聞
馬達加斯加
防疫
狐猴
動物與大環境變遷

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

口罩繩易纏動物 動保籲丟棄前先剪掉

摘錄自2020年9月24日公視報導

英國陸續發現有海鷗的雙腳被口罩繩纏住,就像戴了腳鐐一樣。根據統計,全球目前每個月消耗1000多億個拋棄式口罩和手套,對環境造成很大的負擔;動保團體呼籲民眾在丟掉口罩之前,至少先把繩子剪掉,以免傷害無辜的動物。

南韓環境運動聯合會白娜媛說:「一次性口罩是塑料做的,因此幾乎不能分解,因為無法回收,所以不是焚化就是掩埋,這對自然造成巨大負擔。」

英國皇家防止虐待動物協會10號指出,疫情爆發僅半年,他們就救援900多隻因廢棄口罩或塑膠垃圾受困的動物,呼籲大眾丟口罩前先剪斷鬆緊繩,以免纏住動物。

生物多樣性
國際新聞
口罩
海鷗

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

福島輻射污染土再利用 實驗種出超標作物

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

中國承諾2060年實現碳中和 外媒提五大疑問

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

亂棄口罩掀生態災難 企鵝誤吞N95口罩伏屍巴西沙灘

摘錄自2020年9月25日蘋果新聞報導

近日巴西聖保羅有企鵝伏屍沙灘,經驗屍發現牠胃內藏有一個無法消化的N95口罩,當地保育組織斥人類不當棄置廢物會導致海洋動物傷亡。

當地海洋保育組織Instituto Argonauta解剖時發現,企鵝的胃部有一個完整的N95口罩,相信令企鵝無法消化致死。組織又指出,自4月起在聖保羅北部沿岸已撿到多達113個棄置口罩,當中包括企鵝伏屍的沙灘。組織斥企鵝之死反映不當棄置廢物可導致海洋動物傷亡。

世界自然基金會(WWF)在7月亦警告過不當棄置口罩等個人防護設備(PPE),恐會對環境造成威脅。即使僅有1%的口罩未有妥善處理,已可導致每月有1000萬個棄置口罩流出大自然。

生物多樣性
國際新聞
巴西
企鵝
口罩
誤食

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

福島農夫的七種土質實測:土壤輻射污染越高,作物污染越嚴重

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

日本群馬縣養豬場爆發豬瘟 將撲殺5400頭豬隻

摘錄自2020年09月27日中央通訊社日本報導

日本農林水產省26日宣布,群馬縣高崎市一座養豬場發現豬隻感染家畜傳染病豬瘟(CSF),縣當局將依法撲殺養豬場內全部約5400頭豬隻。

時事通信社報導,這座養豬場為預防感染豬瘟,曾為豬隻接種疫苗,但確診感染的小豬並未接種疫苗。

這是時隔約半年,日本國內養豬場再度爆發豬瘟疫情,也是自2018年日本國內睽違26年首度爆發豬瘟疫情以來,群馬縣內養豬場首度確認感染。

群馬縣當局於發現野生野豬感染豬瘟後,從去年10月起,著手為養豬場接種疫苗。

生物多樣性
循環經濟
國際新聞
日本
豬瘟
經濟動物

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心